mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Type check safe-chain package
This commit is contained in:
parent
d5dc801c00
commit
c88b1a624f
60 changed files with 1179 additions and 33 deletions
|
|
@ -12,6 +12,10 @@ export function getCaCertPath() {
|
|||
return path.join(certFolder, "ca-cert.pem");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} hostname
|
||||
* @returns {{privateKey: string, certificate: string}}
|
||||
*/
|
||||
export function generateCertForHost(hostname) {
|
||||
let existingCert = certCache.get(hostname);
|
||||
if (existingCert) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@ import { generateCertForHost } from "./certUtils.js";
|
|||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import { ui } from "../environment/userInteraction.js";
|
||||
|
||||
/**
|
||||
* @param {import("http").IncomingMessage} req
|
||||
* @param {import("net").Socket} clientSocket
|
||||
* @param {(target: string) => Promise<boolean>} isAllowed
|
||||
*/
|
||||
export function mitmConnect(req, clientSocket, isAllowed) {
|
||||
const { hostname } = new URL(`http://${req.url}`);
|
||||
|
||||
|
|
@ -16,6 +21,7 @@ export function mitmConnect(req, clientSocket, isAllowed) {
|
|||
|
||||
server.on("error", (err) => {
|
||||
ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`);
|
||||
// @ts-expect-error Property 'headersSent' does not exist on type 'Socket'
|
||||
if (!clientSocket.headersSent) {
|
||||
clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
||||
} else if (clientSocket.writable) {
|
||||
|
|
@ -30,10 +36,22 @@ export function mitmConnect(req, clientSocket, isAllowed) {
|
|||
server.emit("connection", clientSocket);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} hostname
|
||||
* @param {(target: string) => Promise<boolean>} isAllowed
|
||||
* @returns {import("https").Server}
|
||||
*/
|
||||
function createHttpsServer(hostname, isAllowed) {
|
||||
const cert = generateCertForHost(hostname);
|
||||
|
||||
/**
|
||||
* @param {import("http").IncomingMessage} req
|
||||
* @param {import("http").ServerResponse} res
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function handleRequest(req, res) {
|
||||
// @ts-expect-error req.url might be undefined
|
||||
const pathAndQuery = getRequestPathAndQuery(req.url);
|
||||
const targetUrl = `https://${hostname}${pathAndQuery}`;
|
||||
|
||||
|
|
@ -58,6 +76,10 @@ function createHttpsServer(hostname, isAllowed) {
|
|||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @returns {*|string}
|
||||
*/
|
||||
function getRequestPathAndQuery(url) {
|
||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
const parsedUrl = new URL(url);
|
||||
|
|
@ -66,6 +88,11 @@ function getRequestPathAndQuery(url) {
|
|||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("http").IncomingMessage} req
|
||||
* @param {string} hostname
|
||||
* @param {import("http").ServerResponse} res
|
||||
*/
|
||||
function forwardRequest(req, hostname, res) {
|
||||
const proxyReq = createProxyRequest(hostname, req, res);
|
||||
|
||||
|
|
@ -88,7 +115,15 @@ function forwardRequest(req, hostname, res) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} hostname
|
||||
* @param {import("http").IncomingMessage} req
|
||||
* @param {import("http").ServerResponse} res
|
||||
*
|
||||
* @returns {import("http").ClientRequest}
|
||||
*/
|
||||
function createProxyRequest(hostname, req, res) {
|
||||
/** @type {import("http").RequestOptions} */
|
||||
const options = {
|
||||
hostname: hostname,
|
||||
port: 443,
|
||||
|
|
@ -97,7 +132,9 @@ function createProxyRequest(hostname, req, res) {
|
|||
headers: { ...req.headers },
|
||||
};
|
||||
|
||||
delete options.headers.host;
|
||||
if (options.headers && "host" in options.headers) {
|
||||
delete options.headers["host"];
|
||||
}
|
||||
|
||||
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
|
||||
if (httpsProxy) {
|
||||
|
|
@ -115,6 +152,7 @@ function createProxyRequest(hostname, req, res) {
|
|||
}
|
||||
});
|
||||
|
||||
// @ts-expect-error statusCode might be undefined
|
||||
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||
proxyRes.pipe(res);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
export const knownRegistries = ["registry.npmjs.org", "registry.yarnpkg.com"];
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @returns {{packageName: string | undefined, version: string | undefined}}
|
||||
*/
|
||||
export function parsePackageFromUrl(url) {
|
||||
let packageName, version, registry;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import * as http from "http";
|
||||
import * as https from "https";
|
||||
|
||||
/**
|
||||
* @param {import("http").IncomingMessage} req
|
||||
* @param {import("http").ServerResponse} res
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function handleHttpProxyRequest(req, res) {
|
||||
// @ts-expect-error req.url might be undefined
|
||||
const url = new URL(req.url);
|
||||
|
||||
// The protocol for the plainHttpProxy should usually only be http:
|
||||
|
|
@ -20,9 +27,11 @@ export function handleHttpProxyRequest(req, res) {
|
|||
|
||||
const proxyRequest = protocol
|
||||
.request(
|
||||
// @ts-expect-error req.url might be undefined
|
||||
req.url,
|
||||
{ method: req.method, headers: req.headers },
|
||||
(proxyRes) => {
|
||||
// @ts-expect-error statusCode might be undefined
|
||||
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||
proxyRes.pipe(res);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import { ui } from "../environment/userInteraction.js";
|
|||
import chalk from "chalk";
|
||||
|
||||
const SERVER_STOP_TIMEOUT_MS = 1000;
|
||||
/**
|
||||
* @type {{port: number | null, blockedRequests: {packageName: string, version: string, url: string}[]}}
|
||||
*/
|
||||
const state = {
|
||||
port: null,
|
||||
blockedRequests: [],
|
||||
|
|
@ -24,6 +27,9 @@ export function createSafeChainProxy() {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Record<string, string>}
|
||||
*/
|
||||
function getSafeChainProxyEnvironmentVariables() {
|
||||
if (!state.port) {
|
||||
return {};
|
||||
|
|
@ -36,6 +42,11 @@ function getSafeChainProxyEnvironmentVariables() {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, string>} env
|
||||
*
|
||||
* @returns {Record<string, string>}
|
||||
*/
|
||||
export function mergeSafeChainProxyEnvironmentVariables(env) {
|
||||
const proxyEnv = getSafeChainProxyEnvironmentVariables();
|
||||
|
||||
|
|
@ -67,6 +78,11 @@ function createProxyServer() {
|
|||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("http").Server} server
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function startServer(server) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Passing port 0 makes the OS assign an available port
|
||||
|
|
@ -86,6 +102,11 @@ function startServer(server) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("http").Server} server
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function stopServer(server) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
|
|
@ -99,10 +120,18 @@ function stopServer(server) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("http").IncomingMessage} req
|
||||
* @param {import("net").Socket} clientSocket
|
||||
* @param {Buffer} head
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleConnect(req, clientSocket, head) {
|
||||
// CONNECT method is used for HTTPS requests
|
||||
// It establishes a tunnel to the server identified by the request URL
|
||||
|
||||
// @ts-expect-error req.url might be undefined
|
||||
if (knownRegistries.some((reg) => req.url.includes(reg))) {
|
||||
// For npm and yarn registries, we want to intercept and inspect the traffic
|
||||
// so we can block packages with malware
|
||||
|
|
@ -113,6 +142,10 @@ function handleConnect(req, clientSocket, head) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function isAllowedUrl(url) {
|
||||
const { packageName, version } = parsePackageFromUrl(url);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import * as net from "net";
|
||||
import { ui } from "../environment/userInteraction.js";
|
||||
|
||||
/**
|
||||
* @param {import("http").IncomingMessage} req
|
||||
* @param {import("net").Socket} clientSocket
|
||||
* @param {Buffer} head
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function tunnelRequest(req, clientSocket, head) {
|
||||
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
|
||||
|
||||
|
|
@ -21,9 +28,17 @@ export function tunnelRequest(req, clientSocket, head) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("http").IncomingMessage} req
|
||||
* @param {import("net").Socket} clientSocket
|
||||
* @param {Buffer} head
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function tunnelRequestToDestination(req, clientSocket, head) {
|
||||
const { port, hostname } = new URL(`http://${req.url}`);
|
||||
|
||||
// @ts-expect-error port from URL is a string but net.connect accepts number
|
||||
const serverSocket = net.connect(port || 443, hostname, () => {
|
||||
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
||||
serverSocket.write(head);
|
||||
|
|
@ -49,11 +64,18 @@ function tunnelRequestToDestination(req, clientSocket, head) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("http").IncomingMessage} req
|
||||
* @param {import("net").Socket} clientSocket
|
||||
* @param {Buffer} head
|
||||
* @param {string} proxyUrl
|
||||
*/
|
||||
function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) {
|
||||
const { port, hostname } = new URL(`http://${req.url}`);
|
||||
const proxy = new URL(proxyUrl);
|
||||
|
||||
// Connect to proxy server
|
||||
// @ts-expect-error net.connect wants port as number but proxy.port is string
|
||||
const proxySocket = net.connect({
|
||||
host: proxy.hostname,
|
||||
port: proxy.port,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue