From 9ad733614f9de494ff952b9c9f72b834bfb8252b Mon Sep 17 00:00:00 2001 From: cel Date: Sat, 28 Mar 2020 15:58:43 -0400 Subject: Restrict access based on Referer --- README.md | 1 + lib/app.js | 38 ++++++++++++++++++++++++++++++++++++++ lib/serve.js | 22 +++++++++++++++++++++- 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 85afdf7..38e6ec3 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ To make config options persistent, set them in `~/.ssb/config`, e.g.: - `auth`: HTTP auth password. default: `null` (no password required) - `allowAddresses`: Array of IP addresses allowed to connect. default: `null` (allow any to connect). Note if host is `localhost` then this setting is useless. - `allowHosts`: Array of hostnames allowed that patchfoo may be connected at, or `*` to allow using any hostname. Default is to allow patchfoo's configured port, at patchfoo's configured host, `localhost`, `127.0.0.1` or `::1`. If hostname includes trailing colon without port, it means use patchfoo's server port. `*` for the port means allow connections at any port. If hostname begins with `.`, subdomains under it are allowed too. +- `trustedReferers`: Array of URL patterns allowed as base of HTTP Referers for POST & PUT requests to patchfoo, or `*` to allow any. Default is `http://` followed by patchfoo's host and port, or `localhost`, `127.0.0.1` or `[::1]` at patchfoo's port. Port may be wildcard (`*`) to allow any port, or blank (trailing `:`) for patchfoo's port. Subdomains can be allowed by beginning the hostname with a period (`.`). patchfoo subpaths which may contain arbitrary blob content are excluded from the set of allowed referers. - `filter`: Filter setting. `"all"` to show all messages. `"invert"` to show messages that would be hidden by the default setting. Otherwise the default setting applies, which is so to only show messages authored or upvoted by yourself or by a feed that you you follow. Exceptions are that if you navigate to a user feed page, you will see messages authored by that feed, and if you navigate to a message page, you will see that message - regardless of the filter setting. The `filter` setting may also be specified per-request as a query string parameter. - `showPrivates`: Whether or not to show private messages. Default is `true`. Overridden by `filter=all`. - `previewVotes`: Whether to preview creating votes/likes/digs (`true`) or publish them immediately (`false`). default: `false` diff --git a/lib/app.js b/lib/app.js index d4f0c98..d779b12 100644 --- a/lib/app.js +++ b/lib/app.js @@ -25,6 +25,7 @@ var fs = require('fs') var mkdirp = require('mkdirp') var Base64URL = require('base64-url') var ssbKeys = require('ssb-keys') +var Url = require('url') var zeros = new Buffer(24); zeros.fill(0) @@ -81,6 +82,27 @@ function App(sbot, config) { } }).filter(Boolean) + this.trustedReferers = u.toArray(conf.trustedReferers || ['http://localhost:/', 'http://127.0.0.1:/', 'http://[::1]:/', 'http://' + this.hostname + '/']) + this.trustedReferersParsed = this.trustedReferers.indexOf('*') > -1 ? [{ + subdomains: true, + host: '', + port: '*', + }] : this.trustedReferers.map(function (pattern) { + var m = /^([a-z0-9\-+]+:)?\/\/+(\.)?(?:\[([0-9a-f:]*)\]|([^:/]*))(:([0-9]*?|\*))?(\/.*?)?$/.exec(pattern) + if (!m) return void console.trace('Unable to parse URL pattern "'+pattern+'"') + var port = !m[5] ? 80 : + !m[6] ? Number(self.port) : + '*' === m[6] ? '*' : Number(m[6]) + if (port !== '*' && isNaN(port)) return void console.trace('Unable to parse port in URL pattern "'+pattern+'". Default port: "'+self.port+'"') + return { + protocol: m[1], + subdomains: !!m[2], + hostname: m[3] || m[4], + port: port, + path: m[7] + } + }).filter(Boolean) + var base = conf.base || '/' this.opts = { base: base, @@ -152,6 +174,22 @@ App.prototype.isAllowedHostHeader = function (hostname) { return false } +App.prototype.getRefererPath = function (referer) { + if (!referer) return + var url = Url.parse(referer) + var port = Number(url.port || 80) + for (var i = 0; i < this.trustedReferersParsed.length; i++) { + var allow = this.trustedReferersParsed[i] + if ((!allow.protocol || allow.protocol === url.protocol) + && (allow.port === '*' || allow.port === port) + && (allow.hostname === '' || allow.hostname === url.hostname || + (allow.subdomains && host.endsWith('.'+allow.hostname))) + && (!allow.path || url.pathname.startsWith(allow.path)) + ) return allow.path ? url.pathname.substr(allow.path.length) : url.pathname + } + return null +} + App.prototype.go = function () { var self = this var server = http.createServer(function (req, res) { diff --git a/lib/serve.js b/lib/serve.js index fdf24f7..414cef5 100644 --- a/lib/serve.js +++ b/lib/serve.js @@ -122,6 +122,19 @@ Serve.prototype.go = function () { Boolean(conf.replyMentionFeeds) if (this.req.method === 'POST' || this.req.method === 'PUT') { + var referer = this.req.headers.referer + var refererPath = this.app.getRefererPath(referer) + if (!refererPath) { + if (!referer) console.error('Missing referer') + else console.error('Referer not allowed: "' + referer + '"') + this.res.writeHead(403) + return this.res.end('Forbidden') + } + if (this.isUnsafePath(refererPath)) { + console.error('Unsafe referer path not allowed: "' + refererPath + '"') + this.res.writeHead(403) + return this.res.end('Forbidden') + } if (/^multipart\/form-data/.test(this.req.headers['content-type'])) { var data = {} var erred @@ -213,6 +226,14 @@ Serve.prototype.go = function () { } } +Serve.prototype.isUnsafePath = function (path) { + return typeof path !== 'string' + || /^&|^%26/.test(path) + || path.indexOf('../') !== -1 + || path.startsWith('/web/') + || path.startsWith('/npm-readme/') +} + Serve.prototype.saveDraft = function (content, cb) { var self = this var data = self.data @@ -2426,7 +2447,6 @@ Serve.prototype.wrapPage = function (title, searchQ) { var done = multicb({pluck: 1, spread: true}) done()(null, h('html', h('head', h('meta', {charset: 'utf-8'}), - h('meta', {name: 'referrer', content: 'no-referrer'}), h('title', title), h('meta', {name: 'viewport', content: 'width=device-width,initial-scale=1'}), h('link', {rel: 'icon', href: render.toUrl('/static/hermie.ico'), type: 'image/x-icon'}), -- cgit v1.2.3