fix: keep Alta bearer out of proxy command lines

This commit is contained in:
2026-08-19 22:26:47 +00:00
parent 582fc46914
commit 808698dc46
15 changed files with 207 additions and 101 deletions
+19 -20
View File
@@ -11,7 +11,7 @@ const APPROVED_DIRECTORY = 'C:\\Program Files\\Alta Proxy Tool';
const APPROVED_HELPER = 'C:\\Program Files\\Alta Proxy Tool\\aware-cam-proxy.exe';
const VALID_HOST = 'tenant.avasecurity.com';
const VALID_DEVICE_ID = '123e4567-e89b-42d3-a456-426614174000';
const SYNTHETIC_COOKIE = 'va=HERMES_SENTINEL_SECRET';
const VALID_USERNAME = 'proxy.operator+apt@example.com';
function loadModule() {
return require(MODULE_PATH);
@@ -56,7 +56,7 @@ function validRequest(overrides = {}) {
return {
deploymentHost: VALID_HOST,
deviceId: VALID_DEVICE_ID,
cookie: SYNTHETIC_COOKIE,
username: VALID_USERNAME,
...overrides
};
}
@@ -72,10 +72,10 @@ test('launches the approved helper directly with exact argv and shell disabled',
assert.deepEqual(calls, [[
APPROVED_HELPER,
['-a', VALID_HOST, '-d', VALID_DEVICE_ID, '-k', SYNTHETIC_COOKIE],
['-a', VALID_HOST, '-u', VALID_USERNAME, '-d', VALID_DEVICE_ID],
{
shell: false,
detached: false,
detached: true,
stdio: 'ignore',
windowsHide: false
}
@@ -89,13 +89,13 @@ test('launches the approved helper directly with exact argv and shell disabled',
});
});
test('passes cookie metacharacters literally in argv without invoking a shell', () => {
const cookie = 'va=abc&whoami|calc.exe;<>()^%!"`$';
test('passes username punctuation literally in argv without invoking a shell', () => {
const username = 'proxy+apt&literal|name@example.com';
const { manager, calls } = createHarness();
manager.launchProxy(validRequest({ cookie }));
manager.launchProxy(validRequest({ username }));
assert.equal(calls[0][1][5], cookie);
assert.equal(calls[0][1][3], username);
assert.equal(calls[0][2].shell, false);
});
@@ -104,8 +104,8 @@ test('rejects the CRLF calc.exe reproducer in every structured input', () => {
const attacks = [
{ deploymentHost: `${VALID_HOST}\r\ncalc.exe` },
{ deviceId: `${VALID_DEVICE_ID}\r\ncalc.exe` },
{ cookie: `${SYNTHETIC_COOKIE}\r\ncalc.exe` },
{ cookie: `${SYNTHETIC_COOKIE}\0calc.exe` }
{ username: `${VALID_USERNAME}\r\ncalc.exe` },
{ username: `${VALID_USERNAME}\0calc.exe` }
];
for (const attack of attacks) {
@@ -178,10 +178,10 @@ test('normalizes a valid Alta hostname to lowercase', () => {
assert.equal(calls[0][1][1], 'tenant.avigilon.com');
});
test('rejects empty, oversized, and control-character cookies', () => {
for (const cookie of ['', 'x'.repeat(4097), 'va=abc\nxyz', 'va=abc\rxyz', 'va=abc\0xyz']) {
test('rejects empty, oversized, non-string, and control-character usernames', () => {
for (const username of ['', 'x'.repeat(255), 42, 'user\nname', 'user\rname', 'user\0name', 'user\u007fname']) {
const { manager } = createHarness();
assert.throws(() => manager.launchProxy(validRequest({ cookie })), /cookie/i);
assert.throws(() => manager.launchProxy(validRequest({ username })), /username/i);
}
});
@@ -205,27 +205,25 @@ test('requires an absolute approved application directory and Windows platform',
);
});
test('redacts the cookie if spawn throws an error containing it', () => {
test('reports a bounded spawn failure without credential redaction machinery', () => {
const { createProxyManager } = loadModule();
const manager = createProxyManager({
appDirectory: APPROVED_DIRECTORY,
fs: { existsSync: () => true },
platform: 'win32',
spawn: () => { throw new Error(`spawn failed for ${SYNTHETIC_COOKIE}`); }
spawn: () => { throw new Error('spawn failed'); }
});
assert.throws(
() => manager.launchProxy(validRequest()),
(error) => {
assert.match(error.message, /spawn failed/);
assert.match(error.message, /\[REDACTED\]/);
assert.doesNotMatch(error.message, /HERMES_SENTINEL_SECRET/);
return true;
}
);
});
test('tracks only safe process metadata and never exposes or persists cookies', () => {
test('tracks only safe process metadata and never exposes usernames', () => {
const { manager } = createHarness();
manager.launchProxy(validRequest());
@@ -236,7 +234,7 @@ test('tracks only safe process metadata and never exposes or persists cookies',
startedAt: 1_777_777_777_777,
status: 'running'
}]);
assert.doesNotMatch(JSON.stringify(tracked), /HERMES_SENTINEL_SECRET/);
assert.doesNotMatch(JSON.stringify(tracked), /proxy\.operator/);
});
test('stopping one tracked process leaves the other tracked process alive', () => {
@@ -309,8 +307,9 @@ test('exit events remove only the matching owned child', () => {
assert.deepEqual(manager.listTrackedProxies().map((item) => item.processId), [4101]);
});
test('source contains no shell launchers, broad process killers, or persistence APIs', () => {
test('source contains no bearer argv, shell launchers, broad process killers, or persistence APIs', () => {
const source = fs.readFileSync(path.join(__dirname, '..', 'src', 'proxy-launch.js'), 'utf8');
assert.doesNotMatch(source, /cookie|bearer|token|-k/i);
assert.doesNotMatch(source, /\b(?:cmd(?:\.exe)?|powershell|taskkill|pkill|wmic)\b/i);
assert.doesNotMatch(source, /\.(?:bat|command)\b/i);
assert.doesNotMatch(source, /(?:writeFile|appendFile|mkdtemp|tmpdir)/);
+41
View File
@@ -0,0 +1,41 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const { createRendererController } = require('../renderer-controller');
function harness(result) {
const calls = [];
const controller = createRendererController({
disconnect: async () => result,
renderConnectionState: (state) => calls.push(['render', state]),
clearDisconnectedState: () => calls.push(['clear']),
showConnectionStatus: (message, type) => calls.push(['status', message, type]),
});
return { controller, calls };
}
test('failed disconnect retains visible connection, device, and proxy state', async () => {
const result = {
success: false,
connected: true,
activeProxies: [{ deviceId: '550e8400-e29b-41d4-a716-446655440000', processId: 4101 }],
message: 'Could not stop every active proxy. The Alta session remains connected.',
};
const { controller, calls } = harness(result);
assert.equal(await controller.disconnect(), result);
assert.deepEqual(calls, [['status', result.message, 'error']]);
});
test('successful disconnect renders disconnected state and clears device state', async () => {
const result = { success: true, connected: false, origin: null, activeProxies: [] };
const { controller, calls } = harness(result);
assert.equal(await controller.disconnect(), result);
assert.deepEqual(calls, [
['render', result],
['clear'],
['status', 'Disconnected from Alta.', 'info'],
]);
});
+46 -4
View File
@@ -79,11 +79,14 @@ test('runtime keeps Alta credentials in main-owned modules and exposes only non-
});
assert.deepEqual(await runtime.getDevices(), { success: true, devices: [{ guid: '550e8400-e29b-41d4-a716-446655440000' }] });
const launched = await runtime.launchProxy('550e8400-e29b-41d4-a716-446655440000');
const launched = await runtime.launchProxy(
'550e8400-e29b-41d4-a716-446655440000',
'proxy.operator@example.com'
);
assert.equal(launched.success, true);
assert.deepEqual(calls[0], {
deploymentHost: 'customer.avasecurity.com',
cookie: 'top-secret-cookie',
username: 'proxy.operator@example.com',
deviceId: '550e8400-e29b-41d4-a716-446655440000',
});
assert.deepEqual(runtime.getConnectionState(), {
@@ -99,6 +102,29 @@ test('runtime keeps Alta credentials in main-owned modules and exposes only non-
assert.equal((await runtime.stopProxy(999)).success, false);
});
test('runtime rejects invalid usernames before calling the proxy manager', async () => {
const sessionStore = createSessionStore();
sessionStore.establish('https://customer.avasecurity.com', 'top-secret-cookie');
const deviceId = '550e8400-e29b-41d4-a716-446655440000';
let launches = 0;
const runtime = new AppRuntime({
sessionStore,
altaClient: { getDevices: async () => [{ guid: deviceId }] },
proxyManager: {
launchProxy() { launches += 1; },
listTrackedProxies() { return []; },
},
});
await runtime.getDevices();
for (const username of ['', 'x'.repeat(255), 'operator@example.com\r\n-k secret', null]) {
const result = await runtime.launchProxy(deviceId, username);
assert.equal(result.success, false);
assert.match(result.message, /username/i);
}
assert.equal(launches, 0);
});
test('bridge authenticates itself before accepting a one-time HMAC cookie request', async () => {
const protect = (value) => Buffer.from(`protected:${value}`);
const unprotect = (value) => Buffer.from(value).toString().slice('protected:'.length);
@@ -223,10 +249,10 @@ test('runtime reconciles exited children and permits relaunch for the same devic
},
});
await runtime.getDevices();
assert.equal((await runtime.launchProxy(deviceId)).processId, 4101);
assert.equal((await runtime.launchProxy(deviceId, 'operator@example.com')).processId, 4101);
tracked.length = 0;
assert.deepEqual(runtime.getConnectionState().activeProxies, []);
assert.equal((await runtime.launchProxy(deviceId)).processId, 4102);
assert.equal((await runtime.launchProxy(deviceId, 'operator@example.com')).processId, 4102);
});
test('disconnect stops every owned proxy before clearing the Alta session', async () => {
@@ -304,8 +330,12 @@ test('preload and renderer expose only narrow, credential-free contracts', () =>
];
for (const method of expectedMethods) assert.match(preload, new RegExp(`\\b${method}\\b`));
assert.doesNotMatch(preload, /downloadAndInstall|download-and-install|onUpdateDownloadProgress|onExtensionCookie/);
assert.match(preload, /launchProxy:\s*\(deviceId, username\)/);
assert.doesNotMatch(preload, /launchProxy:\s*\([^)]*(?:cookie|origin)/i);
assert.doesNotMatch(renderer, /cookieValue|sessionData\.cookies|cookies\s*:/);
assert.match(renderer, /state\.connected\s*&&\s*\(!wasConnected\s*\|\|\s*state\.origin\s*!==\s*previousOrigin\)/);
assert.doesNotMatch(html, /id="cookieKey"|updateProgress|Install Update/);
assert.match(html, /id="altaUsername"/);
assert.match(renderer, /openFixedReleasesPage/);
assert.match(html, /Bridge Pairing/);
});
@@ -327,3 +357,15 @@ test('source policy removes legacy credential IPC, shell launch, broad kill, and
assert.match(read('main.js'), /18247/);
assert.match(read('main.js'), /shell\.openExternal/);
});
test('production source and documentation are GitPeji-only with no active GitHub workflow', () => {
assert.equal(fs.existsSync(path.join(ROOT, '.github', 'workflows', 'deploy-pages.yml')), false);
const sourceAndDocs = [
'main.js', 'preload.js', 'renderer.js', 'renderer-controller.js', 'index.html',
'src/electron-runtime.js', 'src/proxy-launch.js', 'README.md', 'CLAUDE.md',
'docs/security/2026-08-security-baseline.md',
'docs/plans/2026-08-19-apt-security-foundation.md',
].map(read).join('\n');
assert.doesNotMatch(sourceAndDocs, /github/i);
assert.match(sourceAndDocs, /GitPeji/);
});