Skip to content

Commit b3485a3

Browse files
committed
fix: add content-security policy, prevent access to system files
disallow access to system files and set strict content-security to prevent connection to external resources.
1 parent 1815b52 commit b3485a3

9 files changed

Lines changed: 85 additions & 20 deletions

File tree

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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const path = require('path')
66
const markedpp = require('markedpp').default
77
const md = require('markdown-it')
88
const confluencer = require('confluencer')
9+
const { newError, contentSecurityPolicy } = require('./utils.js')
910

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

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

7576
render(filename, title, req._config)
7677
.then((rendered) => {
77-
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+
})
7882
res.write(rendered)
7983
res.end()
8084
})

lib/middlewares.js

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,29 @@ 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 = /^\/(etc|bin|sbin|usr|boot|opt|var|lib|[Cc]:\/[Ww]indows\/[Ss]ystem32)(\/|$)/
2324

2425
/**
2526
* middlewares
2627
*/
2728
const M = {
2829
// only allow access from localhost
29-
forbiddenRemote: function (req, res, next) {
30+
forbiddenRemote: function (req, _res, next) {
3031
if (
3132
['127.0.0.1', '::ffff:127.0.0.1', '::1'].indexOf(
3233
req.connection.remoteAddress
@@ -38,6 +39,17 @@ const M = {
3839
next()
3940
}
4041
},
42+
43+
// forbid access to system paths
44+
forbiddenPaths: function (req, _res, next) {
45+
const reqPath = uri2filename(req.url)
46+
if (FORBIDDEN_RE.test(reqPath)) {
47+
next(newError(403))
48+
return
49+
}
50+
next()
51+
},
52+
4153
// check session
4254
session: (token) => (req, res, next) => {
4355
const cookies = cookie.parse(req.headers.cookie || '')
@@ -56,29 +68,35 @@ const M = {
5668
next(newError(403))
5769
}
5870
},
71+
72+
// load config
5973
config: (appConfig) => (req, res, next) => {
6074
req._config = Object.assign({}, config, appConfig.config)
6175
next()
6276
},
6377
renderConfig,
6478
updateConfig,
79+
6580
// render cheatsheet
66-
cheatsheet: (req, res, next) => {
81+
cheatsheet: (req, _res, next) => {
6782
const file = path.resolve(__dirname, '..', 'test', 'cheatsheet.md')
6883
req.url = filename2uri(file)
6984
next()
7085
},
86+
7187
// redirect to homeDir
7288
home: (req, res) => {
7389
res.redirect(homedir())
7490
},
91+
7592
// unescape an escaped URL
76-
unescape: function (req, res, next) {
93+
unescape: function (req, _res, next) {
7794
req.url = decodeURIComponent(req.url)
7895
next()
7996
},
97+
8098
// check if file exists and if is directory
81-
stat: function (req, res, next) {
99+
stat: function (req, _res, next) {
82100
const filename = uri2filename(req.url)
83101
fs.stat(filename, (err, stats) => {
84102
if (err) {
@@ -89,6 +107,7 @@ const M = {
89107
}
90108
})
91109
},
110+
92111
// try to autocorrect markdown links (e.g. for gitlab)
93112
noextfile: function (req, res, next) {
94113
let path
@@ -117,6 +136,7 @@ const M = {
117136
next()
118137
}
119138
},
139+
120140
// serve files without extension as plaintext files
121141
plaintext: function (req, res, next) {
122142
if (!req.isDirectory && FILETEXT.test(req.url)) {
@@ -135,6 +155,7 @@ const M = {
135155
next()
136156
}
137157
},
158+
138159
// make markdown conversion
139160
markdown: function (req, res, next) {
140161
// transform markdown pages
@@ -145,6 +166,7 @@ const M = {
145166
next()
146167
}
147168
},
169+
148170
// show index for directories
149171
serveIndex: function (req, res, next) {
150172
if (!req.isDirectory) {
@@ -174,6 +196,7 @@ const M = {
174196
next(err)
175197
})
176198
},
199+
177200
// serve static files
178201
serveStatic: function (path, options) {
179202
return function (req, res, next) {
@@ -190,12 +213,13 @@ const M = {
190213
})
191214
}
192215
},
216+
193217
// final 404 handler
194218
four0four: function (req, res, next) {
195219
next(newError(404))
196220
},
197-
// error handler
198221

222+
// error handler
199223
error: function (err, req, res, _next) {
200224
if (!err.status) {
201225
// 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: 3 additions & 2 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,7 +45,8 @@
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

test/exfiltrate.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!-- CSP header prevents remove exfiltration -->
2+
3+
**remote**
4+
5+
<pre id="remote"> </pre>
6+
7+
**local**
8+
9+
<pre id="local"> </pre>
10+
11+
<script>
12+
fetch('https://httpbin.org/get?foo=bar').then(res => res.text()).then((text) => {
13+
document.getElementById('remote').textContent = text
14+
})
15+
fetch('http://localhost:4000/etc/passwd').then(res => res.text()).then((text) => {
16+
document.getElementById('local').textContent = text
17+
})
18+
</script>

0 commit comments

Comments
 (0)