mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
reproduce issue
This commit is contained in:
parent
e7cf3488b7
commit
73a0cdf547
2 changed files with 134 additions and 6 deletions
|
|
@ -10,8 +10,9 @@ import { ui } from "../environment/userInteraction.js";
|
||||||
*/
|
*/
|
||||||
export function tunnelRequest(req, clientSocket, head) {
|
export function tunnelRequest(req, clientSocket, head) {
|
||||||
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
|
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
|
||||||
|
const noProxy = process.env.NO_PROXY || process.env.no_proxy;
|
||||||
|
|
||||||
if (httpsProxy) {
|
if (httpsProxy && !shouldBypassProxy(req.url, noProxy)) {
|
||||||
// If an HTTPS proxy is set, tunnel the request via the proxy
|
// If an HTTPS proxy is set, tunnel the request via the proxy
|
||||||
// This is the system proxy, not the safe-chain proxy
|
// This is the system proxy, not the safe-chain proxy
|
||||||
// The package manager will run via the safe-chain proxy
|
// The package manager will run via the safe-chain proxy
|
||||||
|
|
@ -85,14 +86,23 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) {
|
||||||
|
|
||||||
proxySocket.on("connect", () => {
|
proxySocket.on("connect", () => {
|
||||||
// Send CONNECT request to proxy
|
// Send CONNECT request to proxy
|
||||||
const connectRequest = [
|
const headers = [
|
||||||
`CONNECT ${hostname}:${port || 443} HTTP/1.1`,
|
`CONNECT ${hostname}:${port || 443} HTTP/1.1`,
|
||||||
`Host: ${hostname}:${port || 443}`,
|
`Host: ${hostname}:${port || 443}`,
|
||||||
"",
|
];
|
||||||
"",
|
|
||||||
].join("\r\n");
|
|
||||||
|
|
||||||
proxySocket.write(connectRequest);
|
if (proxy.username || proxy.password) {
|
||||||
|
const auth = Buffer.from(
|
||||||
|
`${decodeURIComponent(proxy.username)}:${decodeURIComponent(
|
||||||
|
proxy.password
|
||||||
|
)}`
|
||||||
|
).toString("base64");
|
||||||
|
headers.push(`Proxy-Authorization: Basic ${auth}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.push("", "");
|
||||||
|
|
||||||
|
proxySocket.write(headers.join("\r\n"));
|
||||||
});
|
});
|
||||||
|
|
||||||
let isConnected = false;
|
let isConnected = false;
|
||||||
|
|
@ -145,3 +155,36 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | undefined} url
|
||||||
|
* @param {string | undefined} noProxy
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function shouldBypassProxy(url, noProxy) {
|
||||||
|
if (!url || !noProxy) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noProxy === "*") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { hostname } = new URL(`http://${url}`);
|
||||||
|
const noProxyList = noProxy.split(",").map((s) => s.trim().toLowerCase());
|
||||||
|
|
||||||
|
return noProxyList.some((noProxyItem) => {
|
||||||
|
if (!noProxyItem) return false;
|
||||||
|
if (noProxyItem === hostname) return true;
|
||||||
|
// Handle domain matching (e.g. .example.com matches sub.example.com)
|
||||||
|
if (noProxyItem.startsWith(".") && hostname.endsWith(noProxyItem))
|
||||||
|
return true;
|
||||||
|
// Handle implicit domain matching (e.g. example.com matches sub.example.com)
|
||||||
|
if (hostname.endsWith(`.${noProxyItem}`)) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
85
test/e2e/proxy-tunneling.e2e.spec.js
Normal file
85
test/e2e/proxy-tunneling.e2e.spec.js
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { describe, it, before, beforeEach, afterEach } from "node:test";
|
||||||
|
import { DockerTestContainer } from "./DockerTestContainer.js";
|
||||||
|
import assert from "node:assert";
|
||||||
|
|
||||||
|
describe("E2E: Safe chain proxy tunneling", () => {
|
||||||
|
let container;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
DockerTestContainer.buildImage();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
container = new DockerTestContainer();
|
||||||
|
await container.start();
|
||||||
|
|
||||||
|
const installationShell = await container.openShell("zsh");
|
||||||
|
await installationShell.runCommand("safe-chain setup");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (container) {
|
||||||
|
await container.stop();
|
||||||
|
container = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should tunnel non-registry traffic via upstream proxy with authentication", async () => {
|
||||||
|
// 1. Configure TinyProxy with Basic Auth
|
||||||
|
const proxySetup = await container.openShell("zsh");
|
||||||
|
await proxySetup.runCommand(
|
||||||
|
`echo 'BasicAuth user password' >> /etc/tinyproxy/tinyproxy.conf`
|
||||||
|
);
|
||||||
|
await proxySetup.runCommand("service tinyproxy restart || tinyproxy");
|
||||||
|
|
||||||
|
// 2. Setup a test script that makes a non-registry request (using curl)
|
||||||
|
const setupShell = await container.openShell("zsh");
|
||||||
|
// We use www.example.com as a stable target
|
||||||
|
await setupShell.runCommand('npm pkg set scripts.test-curl="curl -v -I https://www.example.com"');
|
||||||
|
|
||||||
|
// 3. Run the test script with HTTPS_PROXY set to the authenticated upstream proxy
|
||||||
|
const testShell = await container.openShell("zsh");
|
||||||
|
// Set the upstream proxy with credentials
|
||||||
|
await testShell.runCommand('export HTTPS_PROXY="http://user:password@localhost:8888"');
|
||||||
|
|
||||||
|
// Run the script via npm (which is wrapped by safe-chain)
|
||||||
|
// safe-chain should inject its own proxy, which then tunnels to the upstream proxy
|
||||||
|
const { output, command } = await testShell.runCommand("npm run test-curl");
|
||||||
|
|
||||||
|
// 4. Verify the result
|
||||||
|
// If safe-chain fails to authenticate with upstream, we expect a failure
|
||||||
|
// curl -I returns HTTP 200 OK if successful
|
||||||
|
|
||||||
|
const success = output.includes("HTTP/2 200") || output.includes("HTTP/1.1 200");
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
console.log("Test failed. Output:", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ok(success, "curl should successfully connect to example.com via the authenticated proxy");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should respect NO_PROXY and bypass upstream proxy", async () => {
|
||||||
|
// 1. Setup a test script
|
||||||
|
const setupShell = await container.openShell("zsh");
|
||||||
|
await setupShell.runCommand('npm pkg set scripts.test-curl="curl -v -I https://www.example.com"');
|
||||||
|
|
||||||
|
// 2. Set a BROKEN upstream proxy, but exclude example.com via NO_PROXY
|
||||||
|
const testShell = await container.openShell("zsh");
|
||||||
|
await testShell.runCommand('export HTTPS_PROXY="http://non-existent-proxy:1234"');
|
||||||
|
await testShell.runCommand('export NO_PROXY="www.example.com"');
|
||||||
|
|
||||||
|
// 3. Run the script
|
||||||
|
// If safe-chain ignores NO_PROXY, it will try to use the broken proxy and fail
|
||||||
|
// If it respects NO_PROXY, it will connect directly (which works in the container)
|
||||||
|
const { output } = await testShell.runCommand("npm run test-curl");
|
||||||
|
|
||||||
|
const success = output.includes("HTTP/2 200") || output.includes("HTTP/1.1 200");
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
console.log("NO_PROXY Test failed. Output:", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ok(success, "curl should bypass the broken proxy for NO_PROXY domains");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue