From b5f6fb8379c770afb08e37eb737e02557fd87a6c Mon Sep 17 00:00:00 2001 From: TristanInSec Date: Fri, 10 Apr 2026 17:51:18 -0400 Subject: [PATCH] Allowlist request options forwarded from the WebSocket handler The 'request' action in Aggregator#onSocketData currently passes the entire client-supplied resource object directly to the request library: request(r, emitResponse.bind(this, r)) Before that call the server injects its own Authorization header and proxy-user identity header into r.headers, so every field present on the resource object becomes a client-controllable option on an outbound request that is already carrying server credentials. The request library recognises a number of fields that the WebSocket client has no legitimate reason to set, notably proxy, tunnel, auth, cert, key, ca, pfx, agent, agentOptions, baseUrl, strictSSL, rejectUnauthorized, pool, localAddress, har, aws, oauth, hawk, and httpSignature. Introduce buildSafeRequestOptions(resource): it copies only a fixed allowlist of fields (method, url/uri, headers, body/json/form/formData/ multipart, qs/qsStringifyOptions/useQuerystring, encoding, gzip, timeout, followRedirect/followAllRedirects/maxRedirects) onto a fresh object that is then handed to request(). The original resource object continues to be bound into emitResponse so the response path (id, requestOrigin, startTs, etc.) is unchanged. --- server/aggregator.js | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/server/aggregator.js b/server/aggregator.js index 90bd1b47a15..590ad7bf81c 100644 --- a/server/aggregator.js +++ b/server/aggregator.js @@ -91,6 +91,47 @@ Aggregator.prototype.validateSession = function(message) { return true; }; +// Fields that the WebSocket client is allowed to forward into the +// request library's options object. The resource received from the +// client is otherwise attacker-controllable, and the request library +// recognises a number of fields (proxy, tunnel, auth, cert, key, ca, +// pfx, agent, agentOptions, baseUrl, strictSSL, rejectUnauthorized, +// pool, localAddress, har, aws, oauth, hawk, httpSignature, ...) that +// we do not want the client to be able to set — especially not after +// the server has already injected its own Authorization header into +// resource.headers. +var REQUEST_OPTION_ALLOWLIST = [ + 'method', + 'url', + 'uri', + 'headers', + 'body', + 'json', + 'form', + 'formData', + 'multipart', + 'qs', + 'qsStringifyOptions', + 'useQuerystring', + 'encoding', + 'gzip', + 'timeout', + 'followRedirect', + 'followAllRedirects', + 'maxRedirects', +]; + +function buildSafeRequestOptions(resource) { + var safe = {}; + for (var i = 0; i < REQUEST_OPTION_ALLOWLIST.length; i++) { + var key = REQUEST_OPTION_ALLOWLIST[i]; + if (Object.prototype.hasOwnProperty.call(resource, key)) { + safe[key] = resource[key]; + } + } + return safe; +} + /** * Pushes the ETL Application configuration for templates and plugins to the * FE. These configurations are UI specific and hences need to be supported @@ -333,7 +374,7 @@ function onSocketData(message) { r.headers[this.cdapConfig['security.authentication.proxy.user.identity.header']] = this.connection.userid; } log.debug('[REQUEST]: (method: ' + r.method + ', id: ' + r.id + ', url: ' + r.url + ')'); - request(r, emitResponse.bind(this, r)).on('error', function(err) { + request(buildSafeRequestOptions(r), emitResponse.bind(this, r)).on('error', function(err) { log.error('[ERROR]: (url: ' + r.url + ') ' + err.message); }); break;