aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--lib/app.js38
-rw-r--r--lib/serve.js22
3 files changed, 60 insertions, 1 deletions
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'}),