diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1be4043..427f645 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,14 +9,14 @@ on: branches: [] env: - CI: "true" + CI: 'true' jobs: build: runs-on: ubuntu-latest strategy: matrix: - node: [ 20, 22 ] + node: [24] name: Node ${{ matrix.node }} steps: - uses: actions/checkout@v4 @@ -27,4 +27,3 @@ jobs: - run: npm version - run: npm install --ignore-scripts - run: npm run ci - diff --git a/lib/appConfig.js b/lib/appConfig.js index b383401..385c238 100644 --- a/lib/appConfig.js +++ b/lib/appConfig.js @@ -1,9 +1,9 @@ const os = require('os') const fs = require('fs') const path = require('path') -const mkdirp = require('./mkdirp') const crypto = require('crypto') -const { merge } = require('./utils') +const mkdirp = require('./mkdirp.js') +const { merge } = require('./utils.js') const APP = 'md-fileserver' const CONF = 'config.json' diff --git a/lib/index.js b/lib/index.js index 2f16437..e7fac29 100644 --- a/lib/index.js +++ b/lib/index.js @@ -25,7 +25,7 @@ function setupApp() { mw.error ) - app.use(mw.session(token)).use(mw.error) + app.use(mw.session(token), mw.error) app.get('/config', mw.renderConfig({ highlightStyles })) app.post( @@ -41,6 +41,7 @@ function setupApp() { app .use(mw.forbiddenRemote) .use(mw.unescape) + .use(mw.forbiddenPaths) .use(mw.noextfile) .use(mw.stat) .use(mw.plaintext) diff --git a/lib/markd.js b/lib/markd.js index 53aa2d3..a4c3a2f 100644 --- a/lib/markd.js +++ b/lib/markd.js @@ -3,10 +3,10 @@ const { promisify } = require('util') const fs = require('fs') const fsP = require('fs/promises') const path = require('path') -const markedpp = require('markedpp') +const markedpp = require('markedpp').default const md = require('markdown-it') -const { newError } = require('./utils') const confluencer = require('confluencer') +const { newError, contentSecurityPolicy } = require('./utils.js') const tmpl = fs.readFileSync(path.resolve(__dirname, 'pages', 'template.html')) @@ -75,7 +75,10 @@ function renderRequest(filename, req, res, next, status = 200) { render(filename, title, req._config) .then((rendered) => { - res.writeHead(status, { 'Content-Type': 'text/html; charset=utf-8' }) + res.writeHead(status, { + 'Content-Type': 'text/html; charset=utf-8', + 'Content-Security-Policy': contentSecurityPolicy + }) res.write(rendered) res.end() }) diff --git a/lib/middlewares.js b/lib/middlewares.js index a936b36..efb5920 100644 --- a/lib/middlewares.js +++ b/lib/middlewares.js @@ -5,9 +5,9 @@ const serveIndex = require('serve-index') const serveStatic = require('serve-static') const cookie = require('cookie') -const config = require('../config') -const { renderRequest } = require('./markd') -const { renderConfig, updateConfig } = require('./renderConfig') +const config = require('../config.js') +const { renderRequest } = require('./markd.js') +const { renderConfig, updateConfig } = require('./renderConfig.js') const { newError, homedir, @@ -15,18 +15,20 @@ const { uri2filename, filename2uri, urlWithoutDrive -} = require('./utils') +} = require('./utils.js') const MARKDOWNEXT = /\.(md|mdown|markdown)$/ const FILETEXT = /[/\\]([A-Za-z]+)$/ const FILENOEXT = /[/\\]([^.]+)$/ +const FORBIDDEN_RE = + /^\/(etc|bin|sbin|usr|boot|opt|var|lib|[Cc]:\/[Ww]indows\/[Ss]ystem32)(\/|$)/ /** * middlewares */ const M = { // only allow access from localhost - forbiddenRemote: function (req, res, next) { + forbiddenRemote: function (req, _res, next) { if ( ['127.0.0.1', '::ffff:127.0.0.1', '::1'].indexOf( req.connection.remoteAddress @@ -38,6 +40,17 @@ const M = { next() } }, + + // forbid access to system paths + forbiddenPaths: function (req, _res, next) { + const reqPath = uri2filename(req.url) + if (FORBIDDEN_RE.test(reqPath)) { + next(newError(403)) + return + } + next() + }, + // check session session: (token) => (req, res, next) => { const cookies = cookie.parse(req.headers.cookie || '') @@ -56,29 +69,35 @@ const M = { next(newError(403)) } }, + + // load config config: (appConfig) => (req, res, next) => { req._config = Object.assign({}, config, appConfig.config) next() }, renderConfig, updateConfig, + // render cheatsheet - cheatsheet: (req, res, next) => { + cheatsheet: (req, _res, next) => { const file = path.resolve(__dirname, '..', 'test', 'cheatsheet.md') req.url = filename2uri(file) next() }, + // redirect to homeDir home: (req, res) => { res.redirect(homedir()) }, + // unescape an escaped URL - unescape: function (req, res, next) { + unescape: function (req, _res, next) { req.url = decodeURIComponent(req.url) next() }, + // check if file exists and if is directory - stat: function (req, res, next) { + stat: function (req, _res, next) { const filename = uri2filename(req.url) fs.stat(filename, (err, stats) => { if (err) { @@ -89,6 +108,7 @@ const M = { } }) }, + // try to autocorrect markdown links (e.g. for gitlab) noextfile: function (req, res, next) { let path @@ -117,6 +137,7 @@ const M = { next() } }, + // serve files without extension as plaintext files plaintext: function (req, res, next) { if (!req.isDirectory && FILETEXT.test(req.url)) { @@ -135,6 +156,7 @@ const M = { next() } }, + // make markdown conversion markdown: function (req, res, next) { // transform markdown pages @@ -145,6 +167,7 @@ const M = { next() } }, + // show index for directories serveIndex: function (req, res, next) { if (!req.isDirectory) { @@ -174,6 +197,7 @@ const M = { next(err) }) }, + // serve static files serveStatic: function (path, options) { return function (req, res, next) { @@ -190,12 +214,13 @@ const M = { }) } }, + // final 404 handler four0four: function (req, res, next) { next(newError(404)) }, - // error handler + // error handler error: function (err, req, res, _next) { if (!err.status) { // Error objects diff --git a/lib/renderConfig.js b/lib/renderConfig.js index c96cd1f..475ba2e 100644 --- a/lib/renderConfig.js +++ b/lib/renderConfig.js @@ -1,7 +1,8 @@ const _template = require('lodash/template') const fs = require('fs') const path = require('path') -const { markdown } = require('./markd') +const { markdown } = require('./markd.js') +const { contentSecurityPolicy } = require('./utils.js') function renderConfig({ highlightStyles }) { const styles = fs @@ -22,7 +23,10 @@ function renderConfig({ highlightStyles }) { markdown(req.url, data, req._config) .then((data) => { - res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }) + res.writeHead(200, { + 'Content-Type': 'text/html; charset=utf-8', + 'Content-Security-Policy': contentSecurityPolicy + }) res.write(data) res.end() }) diff --git a/lib/utils.js b/lib/utils.js index 5794752..c58be93 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -22,6 +22,18 @@ const urlWithoutDrive = (uri) => const homedir = () => (isWin32 ? filename2uri(os.homedir()) : os.homedir()) +const contentSecurityPolicy = [ + "default-src 'self' 'unsafe-inline'", + "script-src 'self' 'unsafe-inline'", + "style-src 'self' 'unsafe-inline'", + 'img-src data: *', + 'font-src data: *', + "connect-src 'self'", + 'media-src data: *', + 'object-src data: *', + 'prefetch-src data: *' +].join('; ') + /** * sets error * @param {Error|Number} err @@ -74,5 +86,6 @@ module.exports = { urlWithoutDrive, newError, merge, - requireResolve + requireResolve, + contentSecurityPolicy } diff --git a/lib/watch.js b/lib/watch.js index c1fc55e..6805275 100644 --- a/lib/watch.js +++ b/lib/watch.js @@ -1,7 +1,7 @@ const fs = require('fs') -const mw = require('./middlewares') -const { uri2filename, filename2uri } = require('./utils') const WebSocket = require('ws') +const mw = require('./middlewares.js') +const { uri2filename, filename2uri } = require('./utils.js') const MARKDOWNEXT = mw.MARKDOWNEXT const WATCHOPTS = { persistent: true, recursive: false } diff --git a/package.json b/package.json index 9145350..6e7d3f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "md-fileserver", - "version": "1.10.2", + "version": "1.10.3-0", "description": "Locally view markdown files in a browser", "keywords": [ "fileserver", @@ -45,15 +45,16 @@ "readme": "markedpp --githubid -i README.md -o README.md", "test": "mocha", "test:md": "bin/mdstart.js test/cheatsheet.md", - "test:cnfl": "bin/mdstart.js test/cnfl.md" + "test:cnfl": "bin/mdstart.js test/cnfl.md", + "test:exf": "bin/mdstart.js test/exfiltrate.md" }, "mocha": { "exit": true }, "dependencies": { "@commenthol/markdown-it-katex": "^2.0.8", - "asyncc": "^2.0.7", - "body-parser": "^1.20.3", + "asyncc": "^2.0.9", + "body-parser": "^2.2.0", "confluencer": "^1.5.2", "cookie": "^1.0.2", "express": "^4.21.2", @@ -63,7 +64,7 @@ "markdown-it-abbr": "^2.0.0", "markdown-it-admon": "^1.0.1", "markdown-it-anchor": "^9.2.0", - "markdown-it-attrs": "~4.2.0", + "markdown-it-attrs": "4.2.0", "markdown-it-deflist": "^3.0.0", "markdown-it-emoji": "^3.0.0", "markdown-it-footnote": "^4.0.0", @@ -73,37 +74,40 @@ "markdown-it-sub": "^2.0.0", "markdown-it-sup": "^2.0.0", "markdown-it-task-lists": "^2.1.1", - "markedpp": "^1.4.0", + "markedpp": "^2.0.3", "serve-index": "^1.9.1", - "serve-static": "^1.16.2", - "ws": "^8.18.0" + "serve-static": "^2.2.0", + "ws": "^8.18.3" }, "devDependencies": { - "@babel/core": "^7.26.0", + "@babel/core": "^7.28.5", "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-transform-react-jsx": "^7.25.9", - "@babel/preset-env": "^7.26.0", - "@babel/register": "^7.25.9", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/preset-env": "^7.28.5", + "@babel/register": "^7.28.3", "babel-eslint": "^10.1.0", - "babel-loader": "^9.2.1", + "babel-loader": "^10.0.0", "css-loader": "^7.1.2", - "eslint": "^9.17.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.2.1", - "globals": "^15.14.0", - "katex": "^0.16.19", - "mini-css-extract-plugin": "^2.9.2", - "mocha": "^11.0.1", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "globals": "^16.5.0", + "katex": "^0.16.25", + "mini-css-extract-plugin": "^2.9.4", + "mocha": "^11.7.5", "normalize.css": "^8.0.1", - "npm-run-all2": "^7.0.2", - "rimraf": "^6.0.1", + "npm-run-all2": "^8.0.4", + "rimraf": "^6.1.0", "style-loader": "^4.0.0", - "supertest": "^7.0.0", - "webpack": "^5.97.1", + "supertest": "^7.1.4", + "webpack": "^5.102.1", "webpack-cli": "^6.0.1", - "webpack-dev-server": "^5.2.0" + "webpack-dev-server": "^5.2.2" }, "engine": { "node": ">=10.0.0" + }, + "c4uIgnore": { + "markdown-it-attrs": "4.2.0" } } diff --git a/test/cheatsheet.md b/test/cheatsheet.md index 3ee1a92..a146b1b 100644 --- a/test/cheatsheet.md +++ b/test/cheatsheet.md @@ -392,12 +392,12 @@ img[alt=img100x] {width: 100px; border: 5px solid red; border-radius: 25%}   -  +    - - + +  Using plain html can be used to size images: diff --git a/test/exfiltrate.md b/test/exfiltrate.md new file mode 100644 index 0000000..8a799ac --- /dev/null +++ b/test/exfiltrate.md @@ -0,0 +1,18 @@ + + +**remote** + +
+ +**local** + +
+ +