Implement modification of request headerrs

This commit is contained in:
Sander Declerck 2025-11-07 16:16:37 +01:00
parent 76a1100b8c
commit 3bf7279195
No known key found for this signature in database
3 changed files with 84 additions and 16 deletions

View file

@ -1,3 +1,4 @@
import chalk from "chalk";
import { isMalwarePackage } from "../../scanning/audit/index.js"; import { isMalwarePackage } from "../../scanning/audit/index.js";
import { createInterceptorBuilder } from "./interceptorBuilder.js"; import { createInterceptorBuilder } from "./interceptorBuilder.js";
@ -32,6 +33,15 @@ function buildNpmInterceptor(registry) {
if (await isMalwarePackage(packageName, version)) { if (await isMalwarePackage(packageName, version)) {
req.blockMalware(packageName, version, req.targetUrl); 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(); return builder.build();

View file

@ -3,10 +3,13 @@
* @property {string} targetUrl * @property {string} targetUrl
* @property {(statusCode: number, message: string) => void} blockRequest * @property {(statusCode: number, message: string) => void} blockRequest
* @property {(packageName: string | undefined, version: string | undefined, url: string) => void} blockMalware * @property {(packageName: string | undefined, version: string | undefined, url: string) => void} blockMalware
* @property {(modificationFunc: (headers: NodeJS.Dict<string | string[]>) => void) => void} modifyRequestHeaders
* @property {() => RequestInterceptor} build * @property {() => RequestInterceptor} build
* *
* @typedef {Object} RequestInterceptor * @typedef {Object} RequestInterceptor
* @property {{statusCode: number, message: string} | undefined} blockResponse * @property {{statusCode: number, message: string} | undefined} blockResponse
* @property {(headers: NodeJS.Dict<string | string[]> | undefined) => void} modifyRequestHeaders
* @property {() => boolean} modifiesResponse
*/ */
/** /**
@ -18,6 +21,15 @@ export function createRequestInterceptorBuilder(targetUrl, eventEmitter) {
/** @type {{statusCode: number, message: string} | undefined} */ /** @type {{statusCode: number, message: string} | undefined} */
let blockResponse = undefined; let blockResponse = undefined;
/**
* @type {{
* requestHeaders: Array<(headers: NodeJS.Dict<string | string[]>) => void>
* }}
*/
let modificationFuncs = {
requestHeaders: [],
};
/** /**
* @param {number} statusCode * @param {number} statusCode
* @param {string} message * @param {string} message
@ -47,10 +59,43 @@ export function createRequestInterceptorBuilder(targetUrl, eventEmitter) {
targetUrl, targetUrl,
blockRequest, blockRequest,
blockMalware, blockMalware,
modifyRequestHeaders(modificationFunc) {
modificationFuncs.requestHeaders.push(modificationFunc);
},
build() { build() {
return { return createRequestInterceptor(
blockResponse, blockResponse,
}; modificationFuncs.requestHeaders
);
}, },
}; };
} }
/**
* @param {{statusCode: number, message: string} | undefined} blockResponse
* @param {Array<(headers: NodeJS.Dict<string | string[]>) => void>} requestHeadersModficationFuncs
* @returns {RequestInterceptor}
*/
function createRequestInterceptor(
blockResponse,
requestHeadersModficationFuncs
) {
/**
* @param {NodeJS.Dict<string | string[]> | undefined} headers
*/
function modifyRequestHeaders(headers) {
if (!headers) {
return;
}
for (const modificationFunc of requestHeadersModficationFuncs) {
modificationFunc(headers);
}
}
function modifiesResponse() {
return false;
}
return { blockResponse, modifyRequestHeaders, modifiesResponse };
}

View file

@ -5,6 +5,7 @@ import { ui } from "../environment/userInteraction.js";
/** /**
* @typedef {import("./interceptors/interceptorBuilder.js").Interceptor} Interceptor * @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 pathAndQuery = getRequestPathAndQuery(req.url);
const targetUrl = `https://${hostname}${pathAndQuery}`; const targetUrl = `https://${hostname}${pathAndQuery}`;
const interceptorResult = await interceptor.handleRequest(targetUrl); const requestInterceptor = await interceptor.handleRequest(targetUrl);
const blockResponse = interceptorResult.blockResponse;
if (blockResponse) { if (requestInterceptor.blockResponse) {
ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`); ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`);
res.writeHead(blockResponse.statusCode, blockResponse.message); res.writeHead(
res.end(blockResponse.message); requestInterceptor.blockResponse.statusCode,
requestInterceptor.blockResponse.message
);
res.end(requestInterceptor.blockResponse.message);
return; return;
} }
// Collect request body // Collect request body
forwardRequest(req, hostname, res); forwardRequest(req, hostname, res, requestInterceptor);
} }
const server = https.createServer( const server = https.createServer(
@ -109,9 +112,10 @@ function getRequestPathAndQuery(url) {
* @param {import("http").IncomingMessage} req * @param {import("http").IncomingMessage} req
* @param {string} hostname * @param {string} hostname
* @param {import("http").ServerResponse} res * @param {import("http").ServerResponse} res
* @param {RequestInterceptor} requestInterceptor
*/ */
function forwardRequest(req, hostname, res) { function forwardRequest(req, hostname, res, requestInterceptor) {
const proxyReq = createProxyRequest(hostname, req, res); const proxyReq = createProxyRequest(hostname, req, res, requestInterceptor);
proxyReq.on("error", (err) => { proxyReq.on("error", (err) => {
ui.writeVerbose( ui.writeVerbose(
@ -142,10 +146,17 @@ function forwardRequest(req, hostname, res) {
* @param {string} hostname * @param {string} hostname
* @param {import("http").IncomingMessage} req * @param {import("http").IncomingMessage} req
* @param {import("http").ServerResponse} res * @param {import("http").ServerResponse} res
* @param {RequestInterceptor} requestInterceptor
* *
* @returns {import("http").ClientRequest} * @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} */ /** @type {import("http").RequestOptions} */
const options = { const options = {
hostname: hostname, hostname: hostname,
@ -155,10 +166,6 @@ function createProxyRequest(hostname, req, res) {
headers: { ...req.headers }, headers: { ...req.headers },
}; };
if (options.headers && "host" in options.headers) {
delete options.headers.host;
}
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
if (httpsProxy) { if (httpsProxy) {
options.agent = new HttpsProxyAgent(httpsProxy); options.agent = new HttpsProxyAgent(httpsProxy);
@ -183,7 +190,13 @@ function createProxyRequest(hostname, req, res) {
} }
res.writeHead(proxyRes.statusCode, proxyRes.headers); 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; return proxyReq;