Files
pbakaus_impeccable/tests/detect-url-launch.test.mjs
T
d690349db1 Fix: keep URL basic-auth credentials on the scan origin (#657)
page.authenticate is page-wide, so a cross-origin redirect that then 401s would receive the original credentials. Attach Authorization only to requests for the scan origin.

Written with AI assistance (Cursor); reviewed by maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 05:53:01 +05:00

295 lines
9.6 KiB
JavaScript

import { describe, test, expect, afterEach } from 'bun:test';
import http from 'node:http';
import { launchBrowser, detectUrl, splitScanUrl } from '../cli/engine/engines/browser/detect-url.mjs';
// launchBrowser prefers the system-installed Chrome on Windows to dodge the
// bundled-Chrome GPU crash-loop (issue #372), and keeps the pinned bundled
// build everywhere else. The function takes the puppeteer module as a
// parameter, so a fake lets us assert the launch strategy without a real
// browser or a real OS.
const realPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
function setPlatform(value) {
Object.defineProperty(process, 'platform', { value, configurable: true });
}
afterEach(() => {
Object.defineProperty(process, 'platform', realPlatform);
});
function makePuppeteer({ failChannel = false } = {}) {
const calls = [];
const fakeBrowser = { __fake: true };
return {
calls,
fakeBrowser,
mod: {
default: {
async launch(opts) {
calls.push(opts);
if (failChannel && opts.channel === 'chrome') {
throw new Error('Could not find Chrome (channel: chrome)');
}
return fakeBrowser;
},
},
},
};
}
describe('launchBrowser', () => {
test('Windows: prefers system Chrome via channel:chrome', async () => {
setPlatform('win32');
const p = makePuppeteer();
const browser = await launchBrowser(p.mod, { headless: true, args: ['--foo'] });
expect(browser).toBe(p.fakeBrowser);
expect(p.calls).toHaveLength(1);
expect(p.calls[0].channel).toBe('chrome');
expect(p.calls[0].headless).toBe(true);
expect(p.calls[0].args).toEqual(['--foo']);
});
test('Windows: falls back to bundled when system Chrome is unavailable', async () => {
setPlatform('win32');
const p = makePuppeteer({ failChannel: true });
const browser = await launchBrowser(p.mod, { headless: true, args: [] });
expect(browser).toBe(p.fakeBrowser);
expect(p.calls).toHaveLength(2);
expect(p.calls[0].channel).toBe('chrome'); // first attempt
expect(p.calls[1].channel).toBeUndefined(); // fallback: bundled, no channel
});
test('non-Windows: uses bundled Chrome directly, no channel', async () => {
setPlatform('linux');
const p = makePuppeteer();
const browser = await launchBrowser(p.mod, { headless: true, args: [] });
expect(browser).toBe(p.fakeBrowser);
expect(p.calls).toHaveLength(1);
expect(p.calls[0].channel).toBeUndefined();
});
test('non-Windows: never attempts channel:chrome even if it would succeed', async () => {
setPlatform('darwin');
const p = makePuppeteer();
await launchBrowser(p.mod, {});
expect(p.calls.every(c => c.channel === undefined)).toBe(true);
});
});
describe('splitScanUrl', () => {
test('strips http(s) userinfo and returns credentials', () => {
expect(splitScanUrl('https://user:pass@example.com')).toEqual({
href: 'https://example.com/',
credentials: { username: 'user', password: 'pass' },
});
expect(splitScanUrl('https://user:p%40ss@example.com/path?q=1')).toEqual({
href: 'https://example.com/path?q=1',
credentials: { username: 'user', password: 'p@ss' },
});
expect(splitScanUrl('https://user@example.com')).toEqual({
href: 'https://example.com/',
credentials: { username: 'user', password: '' },
});
expect(splitScanUrl('http://:secret@host.com/')).toEqual({
href: 'http://host.com/',
credentials: { username: '', password: 'secret' },
});
});
test('preserves original string when no userinfo', () => {
expect(splitScanUrl('https://example.com')).toEqual({
href: 'https://example.com',
credentials: null,
});
expect(splitScanUrl('https://example.com/path?email=a@b.com')).toEqual({
href: 'https://example.com/path?email=a@b.com',
credentials: null,
});
});
test('handles IPv6 and non-http(s) URLs', () => {
expect(splitScanUrl('https://user:pass@[::1]:8080/x')).toEqual({
href: 'https://[::1]:8080/x',
credentials: { username: 'user', password: 'pass' },
});
expect(splitScanUrl('file:///tmp/a.html')).toEqual({
href: 'file:///tmp/a.html',
credentials: null,
});
});
test('returns original string for invalid URLs', () => {
expect(splitScanUrl('not a url')).toEqual({
href: 'not a url',
credentials: null,
});
});
});
function makeFakeBrowser() {
const calls = { intercept: false, requestHandler: null, authenticate: [], goto: [] };
const page = {
on(event, handler) {
if (event === 'request') calls.requestHandler = handler;
},
async setViewport() {},
async setRequestInterception() { calls.intercept = true; },
async authenticate(creds) { calls.authenticate.push(creds); },
async goto(url, opts) { calls.goto.push({ url, opts }); },
async evaluate(fn) {
if (typeof fn === 'function' && fn.toString().includes('impeccableDetect')) {
return [{ findings: [{ type: 'low-contrast', detail: 'x', ignoreValue: '', severity: '' }] }];
}
return [];
},
async close() {},
};
return {
calls,
browser: {
async newPage() { return page; },
},
};
}
function fakeRequest(url, calls) {
return {
url: () => url,
headers: () => ({ accept: 'text/html' }),
continue(overrides) {
calls.continues.push({ url, overrides });
return Promise.resolve();
},
};
}
function listen(server) {
return new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
server.off('error', reject);
resolve(`http://127.0.0.1:${server.address().port}/`);
});
});
}
describe('detectUrl credential redaction', () => {
test('scopes Authorization to the scan origin and redacts findings', async () => {
const { calls, browser } = makeFakeBrowser();
calls.continues = [];
const findings = await detectUrl('https://user:p%40ss@example.com/path', {
browser,
visualContrast: false,
contentHidden: false,
});
expect(calls.authenticate).toEqual([]);
expect(calls.intercept).toBe(true);
expect(typeof calls.requestHandler).toBe('function');
expect(calls.goto).toHaveLength(1);
expect(calls.goto[0].url).toBe('https://example.com/path');
const expected = `Basic ${Buffer.from('user:p@ss').toString('base64')}`;
await calls.requestHandler(fakeRequest('https://example.com/path', calls));
await calls.requestHandler(fakeRequest('https://evil.example/steal', calls));
expect(calls.continues[0].overrides.headers.authorization).toBe(expected);
expect(calls.continues[1].overrides).toBeUndefined();
expect(findings.length).toBeGreaterThan(0);
for (const f of findings) {
expect(f.file).toBe('https://example.com/path');
}
});
test('does not intercept when URL has no userinfo', async () => {
const { calls, browser } = makeFakeBrowser();
const url = 'https://example.com/path';
const findings = await detectUrl(url, {
browser,
visualContrast: false,
contentHidden: false,
});
expect(calls.authenticate).toEqual([]);
expect(calls.intercept).toBe(false);
expect(calls.requestHandler).toBe(null);
expect(findings.length).toBeGreaterThan(0);
for (const f of findings) {
expect(f.file).toBe(url);
}
});
});
describe('detectUrl origin-scoped basic auth', () => {
test('does not send URL credentials to a cross-origin redirect that challenges', async () => {
const user = 'qa-scanner';
const pass = 'Hunter2-657-SHOULD-NOT-LEAK';
const expected = `Basic ${Buffer.from(`${user}:${pass}`).toString('base64')}`;
const seenOnB = [];
const serverB = http.createServer((req, res) => {
seenOnB.push(req.headers.authorization || '');
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="b"' });
res.end('b');
});
const urlB = await listen(serverB);
const serverA = http.createServer((req, res) => {
res.writeHead(302, { Location: urlB });
res.end();
});
const urlA = await listen(serverA);
try {
try {
await detectUrl(urlA.replace('http://', `http://${user}:${pass}@`), {
visualContrast: false,
contentHidden: false,
waitUntil: 'domcontentloaded',
});
} catch {
// B's 401 may fail navigation once credentials are withheld.
}
expect(seenOnB.includes(expected)).toBe(false);
} finally {
await Promise.all([
new Promise((resolve) => serverA.close(resolve)),
new Promise((resolve) => serverB.close(resolve)),
]);
}
}, { timeout: 30000 });
test('still authenticates the original scan origin', async () => {
const user = 'qa-scanner';
const pass = 'Hunter2-657-SHOULD-NOT-LEAK';
const expected = `Basic ${Buffer.from(`${user}:${pass}`).toString('base64')}`;
const seen = [];
const server = http.createServer((req, res) => {
seen.push(req.headers.authorization || '');
if (req.headers.authorization !== expected) {
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="a"' });
res.end('no');
return;
}
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<!doctype html><html><body><h1>ok</h1></body></html>');
});
const origin = await listen(server);
try {
await detectUrl(origin.replace('http://', `http://${user}:${pass}@`), {
visualContrast: false,
contentHidden: false,
waitUntil: 'domcontentloaded',
});
expect(seen.includes(expected)).toBe(true);
} finally {
await new Promise((resolve) => server.close(resolve));
}
}, { timeout: 30000 });
});