fix: close APT adversarial runtime gaps
This commit is contained in:
@@ -37,7 +37,7 @@ function makeElement() {
|
||||
};
|
||||
}
|
||||
|
||||
function makePopupHarness({ paired = true, confirmCopy = false, clipboardReject = null } = {}) {
|
||||
function makePopupHarness({ paired = true, confirmCopy = false, clipboardReject = null, validServerProof = true } = {}) {
|
||||
const elements = Object.fromEntries(
|
||||
['tabInfo', 'pairingInfo', 'sendBtn', 'copyBtn', 'statusMsg', 'copyWarning', 'confirmCopy', 'openOptionsBtn']
|
||||
.map((id) => [id, makeElement()])
|
||||
@@ -66,8 +66,18 @@ function makePopupHarness({ paired = true, confirmCopy = false, clipboardReject
|
||||
documentApi,
|
||||
navigatorApi,
|
||||
confirmCopy: () => confirmCopy,
|
||||
cryptoApi: crypto.webcrypto,
|
||||
fetchImpl: async (...args) => {
|
||||
fetchCalls.push(args);
|
||||
if (args[0].endsWith('/challenge')) {
|
||||
const { clientNonce } = JSON.parse(args[1].body);
|
||||
const serverNonce = 'B'.repeat(43);
|
||||
const canonical = JSON.stringify(['apt-server-challenge-v1', clientNonce, serverNonce]);
|
||||
const serverProof = validServerProof
|
||||
? crypto.createHmac('sha256', 'A'.repeat(43)).update(canonical).digest('base64url')
|
||||
: 'C'.repeat(43);
|
||||
return { ok: true, json: async () => ({ success: true, serverNonce, serverProof }) };
|
||||
}
|
||||
return { ok: true, json: async () => ({ success: true }) };
|
||||
}
|
||||
});
|
||||
@@ -122,18 +132,37 @@ test('deployment detection rejects non-HTTPS, bare, and lookalike Alta hosts', (
|
||||
assert.equal(isSupportedDeploymentUrl('https://customer.avasecurity.com.evil.test/path'), false);
|
||||
});
|
||||
|
||||
test('Send to APT uses the paired secret header and exact endpoint', async () => {
|
||||
test('Send to APT authenticates the listener before reading or sending the cookie', async () => {
|
||||
const harness = makePopupHarness({ paired: true });
|
||||
await harness.controller.init();
|
||||
await harness.controller.sendToApt();
|
||||
assert.equal(harness.fetchCalls.length, 1);
|
||||
const [url, request] = harness.fetchCalls[0];
|
||||
assert.equal(harness.fetchCalls.length, 2);
|
||||
const [challengeUrl, challengeRequest] = harness.fetchCalls[0];
|
||||
assert.equal(challengeUrl, 'http://127.0.0.1:18247/challenge');
|
||||
assert.equal(JSON.stringify(challengeRequest).includes('sensitive-va-token'), false);
|
||||
assert.equal(JSON.stringify(challengeRequest).includes('A'.repeat(43)), false);
|
||||
const [url, request] = harness.fetchCalls[1];
|
||||
assert.equal(url, 'http://127.0.0.1:18247/cookie');
|
||||
assert.equal(request.headers['X-APT-Pairing'], 'A'.repeat(43));
|
||||
assert.deepEqual(JSON.parse(request.body), {
|
||||
deploymentUrl: 'https://customer.avasecurity.com',
|
||||
cookieValue: 'sensitive-va-token'
|
||||
});
|
||||
assert.equal(Object.keys(request.headers).some((name) => /pairing/i.test(name)), false);
|
||||
const body = JSON.parse(request.body);
|
||||
assert.equal(body.deploymentUrl, 'https://customer.avasecurity.com');
|
||||
assert.equal(body.cookieValue, 'sensitive-va-token');
|
||||
assert.match(body.clientNonce, /^[A-Za-z0-9_-]{43}$/);
|
||||
assert.equal(body.serverNonce, 'B'.repeat(43));
|
||||
assert.match(body.proof, /^[A-Za-z0-9_-]{43}$/);
|
||||
assert.equal(request.body.includes('A'.repeat(43)), false);
|
||||
});
|
||||
|
||||
test('a port-squatting fake server with an invalid proof receives no cookie or pairing secret', async () => {
|
||||
const harness = makePopupHarness({ paired: true, validServerProof: false });
|
||||
await harness.controller.init();
|
||||
await harness.controller.sendToApt();
|
||||
assert.equal(harness.fetchCalls.length, 1);
|
||||
assert.equal(harness.fetchCalls[0][0], 'http://127.0.0.1:18247/challenge');
|
||||
assert.equal(harness.cookieReads, 0);
|
||||
const network = JSON.stringify(harness.fetchCalls);
|
||||
assert.equal(network.includes('sensitive-va-token'), false);
|
||||
assert.equal(network.includes('A'.repeat(43)), false);
|
||||
});
|
||||
|
||||
test('copy cancellation occurs before cookie access and never writes the token', async () => {
|
||||
|
||||
Reference in New Issue
Block a user