From 3bf7279195e9bbe68df415ef650c60c864226ca9 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 7 Nov 2025 16:16:37 +0100 Subject: [PATCH] Implement modification of request headerrs --- .../interceptors/npmInterceptor.js | 10 ++++ .../interceptors/requestInterceptorBuilder.js | 49 ++++++++++++++++++- .../src/registryProxy/mitmRequestHandler.js | 41 ++++++++++------ 3 files changed, 84 insertions(+), 16 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js index 6e33dd0..97ac15d 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js @@ -1,3 +1,4 @@ +import chalk from "chalk"; import { isMalwarePackage } from "../../scanning/audit/index.js"; import { createInterceptorBuilder } from "./interceptorBuilder.js"; @@ -32,6 +33,15 @@ function buildNpmInterceptor(registry) { if (await isMalwarePackage(packageName, version)) { req.blockMalware(packageName, version, req.targetUrl); } + + req.modifyRequestHeaders((headers) => { + if (headers["accept"]?.includes("application/vnd.npm.install-v1+json")) { + // The npm registry sometimes serves a more compact format that lacks + // the time metadata we need to filter out too new packages. + // Force the registry to return the full metadata by changing the Accept header. + headers["accept"] = "application/json"; + } + }); }); return builder.build(); diff --git a/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js index a8b98c6..e492f57 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js +++ b/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js @@ -3,10 +3,13 @@ * @property {string} targetUrl * @property {(statusCode: number, message: string) => void} blockRequest * @property {(packageName: string | undefined, version: string | undefined, url: string) => void} blockMalware + * @property {(modificationFunc: (headers: NodeJS.Dict) => void) => void} modifyRequestHeaders * @property {() => RequestInterceptor} build * * @typedef {Object} RequestInterceptor * @property {{statusCode: number, message: string} | undefined} blockResponse + * @property {(headers: NodeJS.Dict | undefined) => void} modifyRequestHeaders + * @property {() => boolean} modifiesResponse */ /** @@ -18,6 +21,15 @@ export function createRequestInterceptorBuilder(targetUrl, eventEmitter) { /** @type {{statusCode: number, message: string} | undefined} */ let blockResponse = undefined; + /** + * @type {{ + * requestHeaders: Array<(headers: NodeJS.Dict) => void> + * }} + */ + let modificationFuncs = { + requestHeaders: [], + }; + /** * @param {number} statusCode * @param {string} message @@ -47,10 +59,43 @@ export function createRequestInterceptorBuilder(targetUrl, eventEmitter) { targetUrl, blockRequest, blockMalware, + modifyRequestHeaders(modificationFunc) { + modificationFuncs.requestHeaders.push(modificationFunc); + }, build() { - return { + return createRequestInterceptor( blockResponse, - }; + modificationFuncs.requestHeaders + ); }, }; } + +/** + * @param {{statusCode: number, message: string} | undefined} blockResponse + * @param {Array<(headers: NodeJS.Dict) => void>} requestHeadersModficationFuncs + * @returns {RequestInterceptor} + */ +function createRequestInterceptor( + blockResponse, + requestHeadersModficationFuncs +) { + /** + * @param {NodeJS.Dict | undefined} headers + */ + function modifyRequestHeaders(headers) { + if (!headers) { + return; + } + + for (const modificationFunc of requestHeadersModficationFuncs) { + modificationFunc(headers); + } + } + + function modifiesResponse() { + return false; + } + + return { blockResponse, modifyRequestHeaders, modifiesResponse }; +} diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index c3ad934..a76efb4 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -5,6 +5,7 @@ import { ui } from "../environment/userInteraction.js"; /** * @typedef {import("./interceptors/interceptorBuilder.js").Interceptor} Interceptor + * @typedef {import("./interceptors/requestInterceptorBuilder.js").RequestInterceptor} RequestInterceptor */ /** @@ -68,18 +69,20 @@ function createHttpsServer(hostname, interceptor) { const pathAndQuery = getRequestPathAndQuery(req.url); const targetUrl = `https://${hostname}${pathAndQuery}`; - const interceptorResult = await interceptor.handleRequest(targetUrl); - const blockResponse = interceptorResult.blockResponse; + const requestInterceptor = await interceptor.handleRequest(targetUrl); - if (blockResponse) { + if (requestInterceptor.blockResponse) { ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`); - res.writeHead(blockResponse.statusCode, blockResponse.message); - res.end(blockResponse.message); + res.writeHead( + requestInterceptor.blockResponse.statusCode, + requestInterceptor.blockResponse.message + ); + res.end(requestInterceptor.blockResponse.message); return; } // Collect request body - forwardRequest(req, hostname, res); + forwardRequest(req, hostname, res, requestInterceptor); } const server = https.createServer( @@ -109,9 +112,10 @@ function getRequestPathAndQuery(url) { * @param {import("http").IncomingMessage} req * @param {string} hostname * @param {import("http").ServerResponse} res + * @param {RequestInterceptor} requestInterceptor */ -function forwardRequest(req, hostname, res) { - const proxyReq = createProxyRequest(hostname, req, res); +function forwardRequest(req, hostname, res, requestInterceptor) { + const proxyReq = createProxyRequest(hostname, req, res, requestInterceptor); proxyReq.on("error", (err) => { ui.writeVerbose( @@ -142,10 +146,17 @@ function forwardRequest(req, hostname, res) { * @param {string} hostname * @param {import("http").IncomingMessage} req * @param {import("http").ServerResponse} res + * @param {RequestInterceptor} requestInterceptor * * @returns {import("http").ClientRequest} */ -function createProxyRequest(hostname, req, res) { +function createProxyRequest(hostname, req, res, requestInterceptor) { + const headers = { ...req.headers }; + if (headers.host) { + delete headers.host; + } + requestInterceptor.modifyRequestHeaders(headers); + /** @type {import("http").RequestOptions} */ const options = { hostname: hostname, @@ -155,10 +166,6 @@ function createProxyRequest(hostname, req, res) { headers: { ...req.headers }, }; - if (options.headers && "host" in options.headers) { - delete options.headers.host; - } - const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; if (httpsProxy) { options.agent = new HttpsProxyAgent(httpsProxy); @@ -183,7 +190,13 @@ function createProxyRequest(hostname, req, res) { } res.writeHead(proxyRes.statusCode, proxyRes.headers); - proxyRes.pipe(res); + + if (!requestInterceptor.modifiesResponse) { + // If the response is not being modified, we can + // just pipe without the need for + proxyRes.pipe(res); + } else { + } }); return proxyReq;