mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
288 lines
9.2 KiB
Diff
288 lines
9.2 KiB
Diff
diff --git a/lib/lockdown.js b/lib/lockdown.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..7acc55484421812ddb66edf015cdf0e1125b1cab
|
|
--- /dev/null
|
|
+++ b/lib/lockdown.js
|
|
@@ -0,0 +1,157 @@
|
|
+/*!
|
|
+ * Overleaf request-input lockdown.
|
|
+ *
|
|
+ * Captures parsed request input in symbol-keyed fields and, when
|
|
+ * REQ_LOCKDOWN_MODE is 'warn' or 'throw', turns the public req.body,
|
|
+ * req.query and req.params properties into lockdown accessors:
|
|
+ *
|
|
+ * - reads log a warning ('warn' mode) or throw ('throw' mode); request
|
|
+ * input must be read through parseReq() from @overleaf/validation-tools,
|
|
+ * which validates it against a zod schema, or through getRawReqInput()
|
|
+ * for allowlisted middleware
|
|
+ * - writes always work and are stored in the symbol-keyed fields, so body
|
|
+ * parsers and middleware that assign req.body keep functioning unchanged
|
|
+ *
|
|
+ * With REQ_LOCKDOWN_MODE unset (or 'off') nothing is installed and express
|
|
+ * behaves exactly like the unpatched package.
|
|
+ *
|
|
+ * The symbols are registered in the global symbol registry so that
|
|
+ * @overleaf/validation-tools can read the raw fields without importing
|
|
+ * express internals. Express' own internals (router param processing, the
|
|
+ * query middleware, res.jsonp) go through the helpers below instead of the
|
|
+ * public properties so that only application code trips the lockdown.
|
|
+ *
|
|
+ * Note: unlike the stock properties, the locked accessors are defined
|
|
+ * non-enumerable so that iterating/serializing a request (e.g. in logging)
|
|
+ * does not trip the lockdown.
|
|
+ */
|
|
+
|
|
+'use strict';
|
|
+
|
|
+var RAW_BODY = Symbol.for('overleaf.lockdown.rawBody');
|
|
+var RAW_QUERY = Symbol.for('overleaf.lockdown.rawQuery');
|
|
+var RAW_PARAMS = Symbol.for('overleaf.lockdown.rawParams');
|
|
+var INSTALLED = Symbol.for('overleaf.lockdown.installed');
|
|
+
|
|
+var VALID_MODES = ['off', 'warn', 'throw'];
|
|
+var cachedMode = null;
|
|
+
|
|
+function mode() {
|
|
+ if (cachedMode === null) {
|
|
+ var m = process.env.REQ_LOCKDOWN_MODE || 'off';
|
|
+ cachedMode = VALID_MODES.indexOf(m) === -1 ? 'off' : m;
|
|
+ }
|
|
+ return cachedMode;
|
|
+}
|
|
+
|
|
+/**
|
|
+ * Override the mode for tests. Pass null to re-read REQ_LOCKDOWN_MODE on the
|
|
+ * next access. Only affects requests that have not been handled yet.
|
|
+ */
|
|
+function setModeForTests(m) {
|
|
+ if (m === null) {
|
|
+ cachedMode = null;
|
|
+ } else {
|
|
+ cachedMode = VALID_MODES.indexOf(m) === -1 ? 'off' : m;
|
|
+ }
|
|
+}
|
|
+
|
|
+var warned = new Set();
|
|
+
|
|
+function report(req, field) {
|
|
+ var route = req.route && req.route.path;
|
|
+ var location =
|
|
+ (req.method || 'UNKNOWN') +
|
|
+ ' ' +
|
|
+ (route || req.originalUrl || req.url || 'unknown');
|
|
+ if (mode() === 'warn') {
|
|
+ var key = location + ' ' + field;
|
|
+ if (!warned.has(key)) {
|
|
+ warned.add(key);
|
|
+ var stack = new Error('trace').stack.split('\n').slice(2, 7).join('\n');
|
|
+ console.warn(
|
|
+ 'req-lockdown: raw request input read: req.' +
|
|
+ field +
|
|
+ ' at ' +
|
|
+ location +
|
|
+ '\n' +
|
|
+ stack
|
|
+ );
|
|
+ }
|
|
+ return;
|
|
+ }
|
|
+ var err = new Error(
|
|
+ 'raw request input is forbidden — use parseReq() (read req.' +
|
|
+ field +
|
|
+ ' at ' +
|
|
+ location +
|
|
+ ')'
|
|
+ );
|
|
+ err.code = 'ERR_RAW_REQ_INPUT';
|
|
+ throw err;
|
|
+}
|
|
+
|
|
+/**
|
|
+ * Install the lockdown accessors on a request. Idempotent, no-op in 'off'
|
|
+ * mode. Called from the router, the query middleware and expressInit so that
|
|
+ * every request is covered no matter how the app is assembled.
|
|
+ */
|
|
+function install(req) {
|
|
+ if (mode() === 'off') return;
|
|
+ if (req[INSTALLED]) return;
|
|
+ req[INSTALLED] = true;
|
|
+ lockField(req, 'body', RAW_BODY);
|
|
+ lockField(req, 'query', RAW_QUERY);
|
|
+ lockField(req, 'params', RAW_PARAMS);
|
|
+}
|
|
+
|
|
+function lockField(req, field, symbol) {
|
|
+ // preserve a value assigned before lockdown was installed
|
|
+ if (Object.prototype.hasOwnProperty.call(req, field)) {
|
|
+ req[symbol] = req[field];
|
|
+ delete req[field];
|
|
+ }
|
|
+ Object.defineProperty(req, field, {
|
|
+ configurable: true,
|
|
+ enumerable: false,
|
|
+ get: function () {
|
|
+ report(this, field);
|
|
+ return this[symbol]; // only reached in 'warn' mode
|
|
+ },
|
|
+ set: function (value) {
|
|
+ this[symbol] = value;
|
|
+ },
|
|
+ });
|
|
+}
|
|
+
|
|
+/**
|
|
+ * Effective params/query for express internals: the raw fields once lockdown
|
|
+ * is installed, the plain properties otherwise.
|
|
+ */
|
|
+function getParams(req) {
|
|
+ return req[INSTALLED] ? req[RAW_PARAMS] : req.params;
|
|
+}
|
|
+
|
|
+function getQuery(req) {
|
|
+ return req[INSTALLED] ? req[RAW_QUERY] : req.query;
|
|
+}
|
|
+
|
|
+/**
|
|
+ * Property key under which the router saves/restores params on the request.
|
|
+ */
|
|
+function paramsProp(req) {
|
|
+ return req[INSTALLED] ? RAW_PARAMS : 'params';
|
|
+}
|
|
+
|
|
+module.exports = {
|
|
+ RAW_BODY: RAW_BODY,
|
|
+ RAW_QUERY: RAW_QUERY,
|
|
+ RAW_PARAMS: RAW_PARAMS,
|
|
+ INSTALLED: INSTALLED,
|
|
+ mode: mode,
|
|
+ setModeForTests: setModeForTests,
|
|
+ install: install,
|
|
+ getParams: getParams,
|
|
+ getQuery: getQuery,
|
|
+ paramsProp: paramsProp,
|
|
+};
|
|
diff --git a/lib/middleware/init.js b/lib/middleware/init.js
|
|
index dfd042747bdb971d9760e9575241ddaa1b776cd8..162ec6da452bde5f5384d3d5bd79d5727742ed8f 100644
|
|
--- a/lib/middleware/init.js
|
|
+++ b/lib/middleware/init.js
|
|
@@ -14,6 +14,7 @@
|
|
*/
|
|
|
|
var setPrototypeOf = require('setprototypeof')
|
|
+var lockdown = require('../lockdown')
|
|
|
|
/**
|
|
* Initialization middleware, exposing the
|
|
@@ -27,6 +28,7 @@ var setPrototypeOf = require('setprototypeof')
|
|
|
|
exports.init = function(app){
|
|
return function expressInit(req, res, next){
|
|
+ lockdown.install(req);
|
|
if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express');
|
|
req.res = res;
|
|
res.req = req;
|
|
diff --git a/lib/middleware/query.js b/lib/middleware/query.js
|
|
index 7e9166947aff3be10f7ffec3771c92581ce211da..aad897b7ecd43477c1768e135ac06211a2424d36 100644
|
|
--- a/lib/middleware/query.js
|
|
+++ b/lib/middleware/query.js
|
|
@@ -15,6 +15,7 @@
|
|
var merge = require('utils-merge')
|
|
var parseUrl = require('parseurl');
|
|
var qs = require('qs');
|
|
+var lockdown = require('../lockdown');
|
|
|
|
/**
|
|
* @param {Object} options
|
|
@@ -37,7 +38,9 @@ module.exports = function query(options) {
|
|
}
|
|
|
|
return function query(req, res, next){
|
|
- if (!req.query) {
|
|
+ lockdown.install(req);
|
|
+
|
|
+ if (!lockdown.getQuery(req)) {
|
|
var val = parseUrl(req).query;
|
|
req.query = queryparse(val, opts);
|
|
}
|
|
diff --git a/lib/response.js b/lib/response.js
|
|
index 2b654f4c6629c821bbeaaeee970b199a420fd8b3..103df49d3d94abf017022cdcf4611e62cad84a74 100644
|
|
--- a/lib/response.js
|
|
+++ b/lib/response.js
|
|
@@ -29,6 +29,7 @@ var normalizeType = require('./utils').normalizeType;
|
|
var normalizeTypes = require('./utils').normalizeTypes;
|
|
var setCharset = require('./utils').setCharset;
|
|
var cookie = require('cookie');
|
|
+var lockdown = require('./lockdown');
|
|
var send = require('send');
|
|
var extname = path.extname;
|
|
var mime = send.mime;
|
|
@@ -312,7 +313,7 @@ res.jsonp = function jsonp(obj) {
|
|
var replacer = app.get('json replacer');
|
|
var spaces = app.get('json spaces');
|
|
var body = stringify(val, replacer, spaces, escape)
|
|
- var callback = this.req.query[app.get('jsonp callback name')];
|
|
+ var callback = lockdown.getQuery(this.req)[app.get('jsonp callback name')];
|
|
|
|
// content-type
|
|
if (!this.get('Content-Type')) {
|
|
diff --git a/lib/router/index.js b/lib/router/index.js
|
|
index abb3a6f589e834c5e105f2a96907ed2eb6fc0185..66fef252003098bcc268ef2ebcef0d20b3cce62b 100644
|
|
--- a/lib/router/index.js
|
|
+++ b/lib/router/index.js
|
|
@@ -22,6 +22,7 @@ var deprecate = require('depd')('express');
|
|
var flatten = require('array-flatten');
|
|
var parseUrl = require('parseurl');
|
|
var setPrototypeOf = require('setprototypeof')
|
|
+var lockdown = require('../lockdown');
|
|
|
|
/**
|
|
* Module variables.
|
|
@@ -136,6 +137,8 @@ proto.param = function param(name, fn) {
|
|
proto.handle = function handle(req, res, out) {
|
|
var self = this;
|
|
|
|
+ lockdown.install(req);
|
|
+
|
|
debug('dispatching %s %s', req.method, req.url);
|
|
|
|
var idx = 0;
|
|
@@ -153,9 +156,9 @@ proto.handle = function handle(req, res, out) {
|
|
var stack = self.stack;
|
|
|
|
// manage inter-router variables
|
|
- var parentParams = req.params;
|
|
+ var parentParams = lockdown.getParams(req);
|
|
var parentUrl = req.baseUrl || '';
|
|
- var done = restore(out, req, 'baseUrl', 'next', 'params');
|
|
+ var done = restore(out, req, 'baseUrl', 'next', lockdown.paramsProp(req));
|
|
|
|
// setup next layer
|
|
req.next = next;
|
|
@@ -368,7 +371,7 @@ proto.process_params = function process_params(layer, called, req, res, done) {
|
|
paramIndex = 0;
|
|
key = keys[i++];
|
|
name = key.name;
|
|
- paramVal = req.params[name];
|
|
+ paramVal = lockdown.getParams(req)[name];
|
|
paramCallbacks = params[name];
|
|
paramCalled = called[name];
|
|
|
|
@@ -380,7 +383,7 @@ proto.process_params = function process_params(layer, called, req, res, done) {
|
|
if (paramCalled && (paramCalled.match === paramVal
|
|
|| (paramCalled.error && paramCalled.error !== 'route'))) {
|
|
// restore value
|
|
- req.params[name] = paramCalled.value;
|
|
+ lockdown.getParams(req)[name] = paramCalled.value;
|
|
|
|
// next param
|
|
return param(paramCalled.error);
|
|
@@ -400,7 +403,7 @@ proto.process_params = function process_params(layer, called, req, res, done) {
|
|
var fn = paramCallbacks[paramIndex++];
|
|
|
|
// store updated value
|
|
- paramCalled.value = req.params[key.name];
|
|
+ paramCalled.value = lockdown.getParams(req)[key.name];
|
|
|
|
if (err) {
|
|
// store error
|