diff --git a/lib/server.js b/lib/server.js
index 0dcce212..9fb725d0 100644
--- a/lib/server.js
+++ b/lib/server.js
@@ -1,4 +1,5 @@
const arrayRemove = require('unordered-array-remove')
+const escapeHtml = require('escape-html')
const http = require('http')
const mime = require('mime')
const pump = require('pump')
@@ -78,10 +79,6 @@ function Server (torrent, opts = {}) {
const pathname = new URL(req.url, 'http://example.com').pathname
- if (pathname === '/favicon.ico') {
- return serve404Page()
- }
-
// Allow cross-origin requests (CORS)
if (isOriginAllowed(req)) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
@@ -90,6 +87,13 @@ function Server (torrent, opts = {}) {
// Prevent browser mime-type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff')
+ // Defense-in-depth: Set a strict Content Security Policy to mitigate XSS
+ res.setHeader('Content-Security-Policy', "base-uri 'none'; default-src 'none'; frame-ancestors 'none'; form-action 'none';")
+
+ if (pathname === '/favicon.ico') {
+ return serve404Page()
+ }
+
// Allow CORS requests to specify arbitrary headers, e.g. 'Range',
// by responding to the OPTIONS preflight request with the specified
// origin and requested headers.
@@ -147,11 +151,26 @@ function Server (torrent, opts = {}) {
res.statusCode = 200
res.setHeader('Content-Type', 'text/html')
- const listHtml = torrent.files.map((file, i) => `
${file.path} (${file.length} bytes)`).join('
')
+ const listHtml = torrent.files
+ .map((file, i) => (
+ `
+
+ ${escapeHtml(file.path)}
+
+ (${escapeHtml(file.length)} bytes)
+ `
+ ))
+ .join('
')
const html = getPageHTML(
- `${torrent.name} - WebTorrent`,
- `${torrent.name}
${listHtml}
`
+ `${escapeHtml(torrent.name)} - WebTorrent`,
+ `
+ ${escapeHtml(torrent.name)}
+ ${listHtml}
+ `
)
res.end(html)
}
@@ -160,7 +179,10 @@ function Server (torrent, opts = {}) {
res.statusCode = 404
res.setHeader('Content-Type', 'text/html')
- const html = getPageHTML('404 - Not Found', '404 - Not Found
')
+ const html = getPageHTML(
+ '404 - Not Found',
+ '404 - Not Found
'
+ )
res.end(html)
}
@@ -214,7 +236,10 @@ function Server (torrent, opts = {}) {
function serveMethodNotAllowed () {
res.statusCode = 405
res.setHeader('Content-Type', 'text/html')
- const html = getPageHTML('405 - Method Not Allowed', '405 - Method Not Allowed
')
+ const html = getPageHTML(
+ '405 - Method Not Allowed',
+ '405 - Method Not Allowed
'
+ )
res.end(html)
}
}
@@ -222,8 +247,20 @@ function Server (torrent, opts = {}) {
return server
}
+// NOTE: Arguments must already be HTML-escaped
function getPageHTML (title, pageHtml) {
- return `${title}${pageHtml}`
+ return `
+
+
+
+
+ ${title}
+
+
+ ${pageHtml}
+
+
+ `
}
// From https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
diff --git a/package.json b/package.json
index e409416f..f9ba2d49 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"create-torrent": "^4.0.0",
"debug": "^4.1.0",
"end-of-stream": "^1.1.0",
+ "escape-html": "^1.0.3",
"fs-chunk-store": "^2.0.0",
"http-node": "github:feross/http-node#cddd2872f0020ecf5016f326cf5e58c965eef52a",
"immediate-chunk-store": "^2.0.0",