mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Add interceptors for MITM
This commit is contained in:
parent
0b056e92de
commit
e251908cb3
4 changed files with 123 additions and 11 deletions
|
|
@ -0,0 +1,50 @@
|
||||||
|
/**
|
||||||
|
* @typedef {import('./requestInterceptorBuilder.js').RequestInterceptorBuilder} RequestInterceptorBuilder
|
||||||
|
* @typedef {import('./requestInterceptorBuilder.js').RequestInterceptor} RequestInterceptor
|
||||||
|
*
|
||||||
|
* @typedef {Object} InterceptorBuilder
|
||||||
|
* @property {(requestFunc: (requestHandlerBuilder: RequestInterceptorBuilder) => Promise<void>) => void} onRequest
|
||||||
|
* @property {() => Interceptor} build
|
||||||
|
*
|
||||||
|
* @typedef {Object} Interceptor
|
||||||
|
* @property {(targetUrl: string) => Promise<RequestInterceptor>} handleRequest
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createRequestInterceptorBuilder } from "./requestInterceptorBuilder.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {InterceptorBuilder}
|
||||||
|
*/
|
||||||
|
export function createInterceptorBuilder() {
|
||||||
|
/**
|
||||||
|
* @type {Array<(requestHandlerBuilder: RequestInterceptorBuilder) => Promise<void>>}
|
||||||
|
*/
|
||||||
|
const requestHandlers = [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
onRequest(requestFunc) {
|
||||||
|
requestHandlers.push(requestFunc);
|
||||||
|
},
|
||||||
|
build() {
|
||||||
|
return buildInterceptor(requestHandlers);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<(requestHandlerBuilder: RequestInterceptorBuilder) => Promise<void>>} requestHandlers
|
||||||
|
* @returns {Interceptor}
|
||||||
|
*/
|
||||||
|
function buildInterceptor(requestHandlers) {
|
||||||
|
return {
|
||||||
|
async handleRequest(targetUrl) {
|
||||||
|
const reqInterceptorBuilder = createRequestInterceptorBuilder(targetUrl);
|
||||||
|
|
||||||
|
for (const handler of requestHandlers) {
|
||||||
|
await handler(reqInterceptorBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reqInterceptorBuilder.build();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* @typedef {Object} RequestInterceptorBuilder
|
||||||
|
* @property {string} targetUrl
|
||||||
|
* @property {(statusCode: number, message: string) => void} blockRequest
|
||||||
|
* @property {() => RequestInterceptor} build
|
||||||
|
*
|
||||||
|
* @typedef {Object} RequestInterceptor
|
||||||
|
* @property {{statusCode: number, message: string} | undefined} blockResponse
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} targetUrl
|
||||||
|
* @returns {RequestInterceptorBuilder}
|
||||||
|
*/
|
||||||
|
export function createRequestInterceptorBuilder(targetUrl) {
|
||||||
|
/** @type {{statusCode: number, message: string} | undefined} */
|
||||||
|
let blockResponse = undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
targetUrl,
|
||||||
|
blockRequest(statusCode, message) {
|
||||||
|
blockResponse = { statusCode, message };
|
||||||
|
},
|
||||||
|
build() {
|
||||||
|
return {
|
||||||
|
blockResponse,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,16 @@ import { generateCertForHost } from "./certUtils.js";
|
||||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||||
import { ui } from "../environment/userInteraction.js";
|
import { ui } from "../environment/userInteraction.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import("./interceptors/interceptorBuilder.js").Interceptor} Interceptor
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import("http").IncomingMessage} req
|
* @param {import("http").IncomingMessage} req
|
||||||
* @param {import("http").ServerResponse} clientSocket
|
* @param {import("http").ServerResponse} clientSocket
|
||||||
* @param {(target: string) => Promise<boolean>} isAllowed
|
* @param {Interceptor} interceptor
|
||||||
*/
|
*/
|
||||||
export function mitmConnect(req, clientSocket, isAllowed) {
|
export function mitmConnect(req, clientSocket, interceptor) {
|
||||||
ui.writeVerbose(`Safe-chain: Set up MITM tunnel for ${req.url}`);
|
ui.writeVerbose(`Safe-chain: Set up MITM tunnel for ${req.url}`);
|
||||||
const { hostname } = new URL(`http://${req.url}`);
|
const { hostname } = new URL(`http://${req.url}`);
|
||||||
|
|
||||||
|
|
@ -21,7 +25,7 @@ export function mitmConnect(req, clientSocket, isAllowed) {
|
||||||
// Not subscribing to 'close' event will cause node to throw and crash.
|
// Not subscribing to 'close' event will cause node to throw and crash.
|
||||||
});
|
});
|
||||||
|
|
||||||
const server = createHttpsServer(hostname, isAllowed);
|
const server = createHttpsServer(hostname, interceptor);
|
||||||
|
|
||||||
server.on("error", (err) => {
|
server.on("error", (err) => {
|
||||||
ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`);
|
ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`);
|
||||||
|
|
@ -41,10 +45,10 @@ export function mitmConnect(req, clientSocket, isAllowed) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} hostname
|
* @param {string} hostname
|
||||||
* @param {(target: string) => Promise<boolean>} isAllowed
|
* @param {Interceptor} interceptor
|
||||||
* @returns {import("https").Server}
|
* @returns {import("https").Server}
|
||||||
*/
|
*/
|
||||||
function createHttpsServer(hostname, isAllowed) {
|
function createHttpsServer(hostname, interceptor) {
|
||||||
const cert = generateCertForHost(hostname);
|
const cert = generateCertForHost(hostname);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -64,10 +68,13 @@ function createHttpsServer(hostname, isAllowed) {
|
||||||
const pathAndQuery = getRequestPathAndQuery(req.url);
|
const pathAndQuery = getRequestPathAndQuery(req.url);
|
||||||
const targetUrl = `https://${hostname}${pathAndQuery}`;
|
const targetUrl = `https://${hostname}${pathAndQuery}`;
|
||||||
|
|
||||||
if (!(await isAllowed(targetUrl))) {
|
const interceptorResult = await interceptor.handleRequest(targetUrl);
|
||||||
|
const blockResponse = interceptorResult?.blockResponse;
|
||||||
|
|
||||||
|
if (blockResponse) {
|
||||||
ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`);
|
ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`);
|
||||||
res.writeHead(403, "Forbidden - blocked by safe-chain");
|
res.writeHead(blockResponse.statusCode, blockResponse.message);
|
||||||
res.end("Blocked by safe-chain");
|
res.end(blockResponse.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,19 @@ import { mitmConnect } from "./mitmRequestHandler.js";
|
||||||
import { handleHttpProxyRequest } from "./plainHttpProxy.js";
|
import { handleHttpProxyRequest } from "./plainHttpProxy.js";
|
||||||
import { getCaCertPath } from "./certUtils.js";
|
import { getCaCertPath } from "./certUtils.js";
|
||||||
import { auditChanges } from "../scanning/audit/index.js";
|
import { auditChanges } from "../scanning/audit/index.js";
|
||||||
import { knownJsRegistries, knownPipRegistries, parsePackageFromUrl } from "./parsePackageFromUrl.js";
|
import {
|
||||||
import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
|
knownJsRegistries,
|
||||||
|
knownPipRegistries,
|
||||||
|
parsePackageFromUrl,
|
||||||
|
} from "./parsePackageFromUrl.js";
|
||||||
|
import {
|
||||||
|
getEcoSystem,
|
||||||
|
ECOSYSTEM_JS,
|
||||||
|
ECOSYSTEM_PY,
|
||||||
|
} from "../config/settings.js";
|
||||||
import { ui } from "../environment/userInteraction.js";
|
import { ui } from "../environment/userInteraction.js";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
import { createInterceptorBuilder } from "./interceptors/interceptorBuilder.js";
|
||||||
|
|
||||||
const SERVER_STOP_TIMEOUT_MS = 1000;
|
const SERVER_STOP_TIMEOUT_MS = 1000;
|
||||||
/**
|
/**
|
||||||
|
|
@ -143,7 +152,7 @@ function handleConnect(req, clientSocket, head) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isKnownRegistry) {
|
if (isKnownRegistry) {
|
||||||
mitmConnect(req, clientSocket, isAllowedUrl);
|
mitmConnect(req, clientSocket, createMitmInterceptor());
|
||||||
} else {
|
} else {
|
||||||
// For other hosts, just tunnel the request to the destination tcp socket
|
// For other hosts, just tunnel the request to the destination tcp socket
|
||||||
ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`);
|
ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`);
|
||||||
|
|
@ -151,6 +160,22 @@ function handleConnect(req, clientSocket, head) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {import("./interceptors/interceptorBuilder.js").Interceptor}
|
||||||
|
*/
|
||||||
|
function createMitmInterceptor() {
|
||||||
|
const builder = createInterceptorBuilder();
|
||||||
|
|
||||||
|
builder.onRequest(async (req) => {
|
||||||
|
if (!(await isAllowedUrl(req.targetUrl))) {
|
||||||
|
req.blockRequest(403, "Forbidden - blocked by safe-chain");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} url
|
* @param {string} url
|
||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue