Skip to content

Commit 9c0d862

Browse files
authored
Merge pull request #25 from commenthol/fix-csp
Fix csp
2 parents 0eb6e7e + ae87025 commit 9c0d862

11 files changed

Lines changed: 118 additions & 51 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ on:
99
branches: []
1010

1111
env:
12-
CI: "true"
12+
CI: 'true'
1313

1414
jobs:
1515
build:
1616
runs-on: ubuntu-latest
1717
strategy:
1818
matrix:
19-
node: [ 20, 22 ]
19+
node: [24]
2020
name: Node ${{ matrix.node }}
2121
steps:
2222
- uses: actions/checkout@v4
@@ -27,4 +27,3 @@ jobs:
2727
- run: npm version
2828
- run: npm install --ignore-scripts
2929
- run: npm run ci
30-

lib/appConfig.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
const os = require('os')
22
const fs = require('fs')
33
const path = require('path')
4-
const mkdirp = require('./mkdirp')
54
const crypto = require('crypto')
6-
const { merge } = require('./utils')
5+
const mkdirp = require('./mkdirp.js')
6+
const { merge } = require('./utils.js')
77

88
const APP = 'md-fileserver'
99
const CONF = 'config.json'

lib/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function setupApp() {
2525
mw.error
2626
)
2727

28-
app.use(mw.session(token)).use(mw.error)
28+
app.use(mw.session(token), mw.error)
2929

3030
app.get('/config', mw.renderConfig({ highlightStyles }))
3131
app.post(
@@ -41,6 +41,7 @@ function setupApp() {
4141
app
4242
.use(mw.forbiddenRemote)
4343
.use(mw.unescape)
44+
.use(mw.forbiddenPaths)
4445
.use(mw.noextfile)
4546
.use(mw.stat)
4647
.use(mw.plaintext)

lib/markd.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ const { promisify } = require('util')
33
const fs = require('fs')
44
const fsP = require('fs/promises')
55
const path = require('path')
6-
const markedpp = require('markedpp')
6+
const markedpp = require('markedpp').default
77
const md = require('markdown-it')
8-
const { newError } = require('./utils')
98
const confluencer = require('confluencer')
9+
const { newError, contentSecurityPolicy } = require('./utils.js')
1010

1111
const tmpl = fs.readFileSync(path.resolve(__dirname, 'pages', 'template.html'))
1212

@@ -75,7 +75,10 @@ function renderRequest(filename, req, res, next, status = 200) {
7575

7676
render(filename, title, req._config)
7777
.then((rendered) => {
78-
res.writeHead(status, { 'Content-Type': 'text/html; charset=utf-8' })
78+
res.writeHead(status, {
79+
'Content-Type': 'text/html; charset=utf-8',
80+
'Content-Security-Policy': contentSecurityPolicy
81+
})
7982
res.write(rendered)
8083
res.end()
8184
})

lib/middlewares.js

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,30 @@ const serveIndex = require('serve-index')
55
const serveStatic = require('serve-static')
66
const cookie = require('cookie')
77

8-
const config = require('../config')
9-
const { renderRequest } = require('./markd')
10-
const { renderConfig, updateConfig } = require('./renderConfig')
8+
const config = require('../config.js')
9+
const { renderRequest } = require('./markd.js')
10+
const { renderConfig, updateConfig } = require('./renderConfig.js')
1111
const {
1212
newError,
1313
homedir,
1414
drive,
1515
uri2filename,
1616
filename2uri,
1717
urlWithoutDrive
18-
} = require('./utils')
18+
} = require('./utils.js')
1919

2020
const MARKDOWNEXT = /\.(md|mdown|markdown)$/
2121
const FILETEXT = /[/\\]([A-Za-z]+)$/
2222
const FILENOEXT = /[/\\]([^.]+)$/
23+
const FORBIDDEN_RE =
24+
/^\/(etc|bin|sbin|usr|boot|opt|var|lib|[Cc]:\/[Ww]indows\/[Ss]ystem32)(\/|$)/
2325

2426
/**
2527
* middlewares
2628
*/
2729
const M = {
2830
// only allow access from localhost
29-
forbiddenRemote: function (req, res, next) {
31+
forbiddenRemote: function (req, _res, next) {
3032
if (
3133
['127.0.0.1', '::ffff:127.0.0.1', '::1'].indexOf(
3234
req.connection.remoteAddress
@@ -38,6 +40,17 @@ const M = {
3840
next()
3941
}
4042
},
43+
44+
// forbid access to system paths
45+
forbiddenPaths: function (req, _res, next) {
46+
const reqPath = uri2filename(req.url)
47+
if (FORBIDDEN_RE.test(reqPath)) {
48+
next(newError(403))
49+
return
50+
}
51+
next()
52+
},
53+
4154
// check session
4255
session: (token) => (req, res, next) => {
4356
const cookies = cookie.parse(req.headers.cookie || '')
@@ -56,29 +69,35 @@ const M = {
5669
next(newError(403))
5770
}
5871
},
72+
73+
// load config
5974
config: (appConfig) => (req, res, next) => {
6075
req._config = Object.assign({}, config, appConfig.config)
6176
next()
6277
},
6378
renderConfig,
6479
updateConfig,
80+
6581
// render cheatsheet
66-
cheatsheet: (req, res, next) => {
82+
cheatsheet: (req, _res, next) => {
6783
const file = path.resolve(__dirname, '..', 'test', 'cheatsheet.md')
6884
req.url = filename2uri(file)
6985
next()
7086
},
87+
7188
// redirect to homeDir
7289
home: (req, res) => {
7390
res.redirect(homedir())
7491
},
92+
7593
// unescape an escaped URL
76-
unescape: function (req, res, next) {
94+
unescape: function (req, _res, next) {
7795
req.url = decodeURIComponent(req.url)
7896
next()
7997
},
98+
8099
// check if file exists and if is directory
81-
stat: function (req, res, next) {
100+
stat: function (req, _res, next) {
82101
const filename = uri2filename(req.url)
83102
fs.stat(filename, (err, stats) => {
84103
if (err) {
@@ -89,6 +108,7 @@ const M = {
89108
}
90109
})
91110
},
111+
92112
// try to autocorrect markdown links (e.g. for gitlab)
93113
noextfile: function (req, res, next) {
94114
let path
@@ -117,6 +137,7 @@ const M = {
117137
next()
118138
}
119139
},
140+
120141
// serve files without extension as plaintext files
121142
plaintext: function (req, res, next) {
122143
if (!req.isDirectory && FILETEXT.test(req.url)) {
@@ -135,6 +156,7 @@ const M = {
135156
next()
136157
}
137158
},
159+
138160
// make markdown conversion
139161
markdown: function (req, res, next) {
140162
// transform markdown pages
@@ -145,6 +167,7 @@ const M = {
145167
next()
146168
}
147169
},
170+
148171
// show index for directories
149172
serveIndex: function (req, res, next) {
150173
if (!req.isDirectory) {
@@ -174,6 +197,7 @@ const M = {
174197
next(err)
175198
})
176199
},
200+
177201
// serve static files
178202
serveStatic: function (path, options) {
179203
return function (req, res, next) {
@@ -190,12 +214,13 @@ const M = {
190214
})
191215
}
192216
},
217+
193218
// final 404 handler
194219
four0four: function (req, res, next) {
195220
next(newError(404))
196221
},
197-
// error handler
198222

223+
// error handler
199224
error: function (err, req, res, _next) {
200225
if (!err.status) {
201226
// Error objects

lib/renderConfig.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
const _template = require('lodash/template')
22
const fs = require('fs')
33
const path = require('path')
4-
const { markdown } = require('./markd')
4+
const { markdown } = require('./markd.js')
5+
const { contentSecurityPolicy } = require('./utils.js')
56

67
function renderConfig({ highlightStyles }) {
78
const styles = fs
@@ -22,7 +23,10 @@ function renderConfig({ highlightStyles }) {
2223

2324
markdown(req.url, data, req._config)
2425
.then((data) => {
25-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
26+
res.writeHead(200, {
27+
'Content-Type': 'text/html; charset=utf-8',
28+
'Content-Security-Policy': contentSecurityPolicy
29+
})
2630
res.write(data)
2731
res.end()
2832
})

lib/utils.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ const urlWithoutDrive = (uri) =>
2222

2323
const homedir = () => (isWin32 ? filename2uri(os.homedir()) : os.homedir())
2424

25+
const contentSecurityPolicy = [
26+
"default-src 'self' 'unsafe-inline'",
27+
"script-src 'self' 'unsafe-inline'",
28+
"style-src 'self' 'unsafe-inline'",
29+
'img-src data: *',
30+
'font-src data: *',
31+
"connect-src 'self'",
32+
'media-src data: *',
33+
'object-src data: *',
34+
'prefetch-src data: *'
35+
].join('; ')
36+
2537
/**
2638
* sets error
2739
* @param {Error|Number} err
@@ -74,5 +86,6 @@ module.exports = {
7486
urlWithoutDrive,
7587
newError,
7688
merge,
77-
requireResolve
89+
requireResolve,
90+
contentSecurityPolicy
7891
}

lib/watch.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const fs = require('fs')
2-
const mw = require('./middlewares')
3-
const { uri2filename, filename2uri } = require('./utils')
42
const WebSocket = require('ws')
3+
const mw = require('./middlewares.js')
4+
const { uri2filename, filename2uri } = require('./utils.js')
55

66
const MARKDOWNEXT = mw.MARKDOWNEXT
77
const WATCHOPTS = { persistent: true, recursive: false }

package.json

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "md-fileserver",
3-
"version": "1.10.2",
3+
"version": "1.10.3-0",
44
"description": "Locally view markdown files in a browser",
55
"keywords": [
66
"fileserver",
@@ -45,15 +45,16 @@
4545
"readme": "markedpp --githubid -i README.md -o README.md",
4646
"test": "mocha",
4747
"test:md": "bin/mdstart.js test/cheatsheet.md",
48-
"test:cnfl": "bin/mdstart.js test/cnfl.md"
48+
"test:cnfl": "bin/mdstart.js test/cnfl.md",
49+
"test:exf": "bin/mdstart.js test/exfiltrate.md"
4950
},
5051
"mocha": {
5152
"exit": true
5253
},
5354
"dependencies": {
5455
"@commenthol/markdown-it-katex": "^2.0.8",
55-
"asyncc": "^2.0.7",
56-
"body-parser": "^1.20.3",
56+
"asyncc": "^2.0.9",
57+
"body-parser": "^2.2.0",
5758
"confluencer": "^1.5.2",
5859
"cookie": "^1.0.2",
5960
"express": "^4.21.2",
@@ -63,7 +64,7 @@
6364
"markdown-it-abbr": "^2.0.0",
6465
"markdown-it-admon": "^1.0.1",
6566
"markdown-it-anchor": "^9.2.0",
66-
"markdown-it-attrs": "~4.2.0",
67+
"markdown-it-attrs": "4.2.0",
6768
"markdown-it-deflist": "^3.0.0",
6869
"markdown-it-emoji": "^3.0.0",
6970
"markdown-it-footnote": "^4.0.0",
@@ -73,37 +74,40 @@
7374
"markdown-it-sub": "^2.0.0",
7475
"markdown-it-sup": "^2.0.0",
7576
"markdown-it-task-lists": "^2.1.1",
76-
"markedpp": "^1.4.0",
77+
"markedpp": "^2.0.3",
7778
"serve-index": "^1.9.1",
78-
"serve-static": "^1.16.2",
79-
"ws": "^8.18.0"
79+
"serve-static": "^2.2.0",
80+
"ws": "^8.18.3"
8081
},
8182
"devDependencies": {
82-
"@babel/core": "^7.26.0",
83+
"@babel/core": "^7.28.5",
8384
"@babel/plugin-proposal-class-properties": "^7.18.6",
84-
"@babel/plugin-transform-react-jsx": "^7.25.9",
85-
"@babel/preset-env": "^7.26.0",
86-
"@babel/register": "^7.25.9",
85+
"@babel/plugin-transform-react-jsx": "^7.27.1",
86+
"@babel/preset-env": "^7.28.5",
87+
"@babel/register": "^7.28.3",
8788
"babel-eslint": "^10.1.0",
88-
"babel-loader": "^9.2.1",
89+
"babel-loader": "^10.0.0",
8990
"css-loader": "^7.1.2",
90-
"eslint": "^9.17.0",
91-
"eslint-config-prettier": "^9.1.0",
92-
"eslint-plugin-prettier": "^5.2.1",
93-
"globals": "^15.14.0",
94-
"katex": "^0.16.19",
95-
"mini-css-extract-plugin": "^2.9.2",
96-
"mocha": "^11.0.1",
91+
"eslint": "^9.39.1",
92+
"eslint-config-prettier": "^10.1.8",
93+
"eslint-plugin-prettier": "^5.5.4",
94+
"globals": "^16.5.0",
95+
"katex": "^0.16.25",
96+
"mini-css-extract-plugin": "^2.9.4",
97+
"mocha": "^11.7.5",
9798
"normalize.css": "^8.0.1",
98-
"npm-run-all2": "^7.0.2",
99-
"rimraf": "^6.0.1",
99+
"npm-run-all2": "^8.0.4",
100+
"rimraf": "^6.1.0",
100101
"style-loader": "^4.0.0",
101-
"supertest": "^7.0.0",
102-
"webpack": "^5.97.1",
102+
"supertest": "^7.1.4",
103+
"webpack": "^5.102.1",
103104
"webpack-cli": "^6.0.1",
104-
"webpack-dev-server": "^5.2.0"
105+
"webpack-dev-server": "^5.2.2"
105106
},
106107
"engine": {
107108
"node": ">=10.0.0"
109+
},
110+
"c4uIgnore": {
111+
"markdown-it-attrs": "4.2.0"
108112
}
109113
}

test/cheatsheet.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -392,12 +392,12 @@ img[alt=img100x] {width: 100px; border: 5px solid red; border-radius: 25%}
392392

393393
![Alt Text](path_to/img.png)
394394
![Alt Text](path_to/img.png "Optional Title")
395-
![Alt Text](http://placehold.it/150x100)
395+
![Alt Text](https://picsum.photos/200/200)
396396
![broken image](http://localhost/my-broken-image)
397397

398398
![Alt Text](path_to/img.png)
399-
![Alt Text](path_to/img.png "Optional Title")
400-
![Alt Text](http://placehold.it/150x100)
399+
![Alt Text](path_to/img.png 'Optional Title')
400+
![Alt Text](https://picsum.photos/200/200)
401401
![broken image](http://localhost/my-broken-image)
402402

403403
Using plain html can be used to size images:

0 commit comments

Comments
 (0)