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",