fix(stealth): address security review findings

1. HIGH — Function.toString Map exfiltration:
   Replaced Map with WeakMap + bound methods. A malicious page could
   monkeypatch Map.prototype.has to capture the override store, then
   use it to cloak malicious functions as [native code]. WeakMap with
   pre-bound has/get methods prevents this side-channel.

2. MEDIUM — Static GPU fingerprint:
   Default GPU renderer now randomly selects from 5 common Apple chip
   variants (M1, M1 Pro, M1 Max, M2, M3) per session. Prevents sites
   from building a static GStack-specific fingerprint signature.

3. Tests updated: 54 total (35 unit + 19 e2e), 0 failures.
   Added tests for WeakMap usage and GPU randomization.
This commit is contained in:
gstack
2026-04-21 03:08:06 +00:00
parent 8df1c003b5
commit 1eae837260
3 changed files with 39 additions and 7 deletions
+19 -3
View File
@@ -53,8 +53,17 @@ export async function applyStealthPatches(
gpuVendor?: string;
},
): Promise<void> {
// Default GPU strings match common real-world Mac hardware.
// Vary slightly across sessions to avoid creating a static fingerprint.
const gpuVariants = [
'ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)',
'ANGLE (Apple, Apple M2, OpenGL 4.1)',
'ANGLE (Apple, Apple M1, OpenGL 4.1)',
'ANGLE (Apple, Apple M3, OpenGL 4.1)',
'ANGLE (Apple, Apple M1 Max, OpenGL 4.1)',
];
const gpuVendor = options?.gpuVendor ?? 'Google Inc. (Apple)';
const gpuRenderer = options?.gpuRenderer ?? 'ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)';
const gpuRenderer = options?.gpuRenderer ?? gpuVariants[Math.floor(Math.random() * gpuVariants.length)];
await context.addInitScript(
([vendor, renderer]: [string, string]) => {
@@ -259,11 +268,18 @@ export async function applyStealthPatches(
// 9. FUNCTION toString PROTECTION
// ========================================
// Make overridden functions look native to .toString() checks.
// SECURITY: Use a WeakMap with a frozen lookup to prevent malicious pages
// from exfiltrating the map via Map.prototype.has/get monkeypatching.
// WeakMap doesn't iterate and can't be fully leaked via prototype hooks.
const nativeStr = Function.prototype.toString;
const overrides = new Map<Function, string>();
const overrides = new WeakMap<Function, string>();
// Freeze a reference to the original WeakMap methods before any page
// script can monkeypatch them.
const wmHas = WeakMap.prototype.has.bind(overrides);
const wmGet = WeakMap.prototype.get.bind(overrides);
Function.prototype.toString = function () {
if (overrides.has(this)) return overrides.get(this)!;
if (wmHas(this)) return wmGet(this)!;
return nativeStr.call(this);
};
overrides.set(Function.prototype.toString, 'function toString() { [native code] }');
+3 -3
View File
@@ -65,7 +65,7 @@ describe('stealth e2e — fingerprint verification', () => {
expect(vendor).not.toContain('SwiftShader');
});
test('WebGL renderer is spoofed to Apple M1 Pro', async () => {
test('WebGL renderer is spoofed to an Apple chip', async () => {
const renderer = await page.evaluate(() => {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl');
@@ -75,7 +75,7 @@ describe('stealth e2e — fingerprint verification', () => {
return gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);
});
expect(renderer).toBeTruthy();
expect(renderer).toContain('Apple M1 Pro');
expect(renderer).toMatch(/Apple.*M[123]/);
expect(renderer).not.toContain('SwiftShader');
expect(renderer).not.toContain('llvmpipe');
});
@@ -91,7 +91,7 @@ describe('stealth e2e — fingerprint verification', () => {
});
// WebGL2 might not be available in all environments
if (renderer !== null) {
expect(renderer).toContain('Apple M1 Pro');
expect(renderer).toMatch(/Apple.*M[123]/);
}
});
+17 -1
View File
@@ -110,6 +110,21 @@ describe('init script coverage', () => {
expect(source).toContain('[native code]');
});
test('uses WeakMap (not Map) for toString overrides to prevent exfiltration', () => {
// Security: Map can be exfiltrated via Map.prototype.has monkeypatching.
// WeakMap with bound methods prevents this attack vector.
expect(source).toContain('new WeakMap');
expect(source).toContain('WeakMap.prototype.has.bind');
expect(source).toContain('WeakMap.prototype.get.bind');
// Must NOT use plain Map for the override store
expect(source).not.toMatch(/new Map[<(]/);
});
test('GPU renderer varies across sessions (anti-fingerprint)', () => {
expect(source).toContain('gpuVariants');
expect(source).toContain('Math.random');
});
test('handles mediaDevices for containers', () => {
expect(source).toContain('mediaDevices');
expect(source).toContain('enumerateDevices');
@@ -184,7 +199,8 @@ describe('applyStealthPatches API', () => {
await applyStealthPatches(mockContext);
const [vendor, renderer] = receivedArg as [string, string];
expect(vendor).toContain('Apple');
expect(renderer).toContain('M1 Pro');
// Renderer varies across sessions but should always be an Apple chip
expect(renderer).toMatch(/Apple.*M[123]/);
});
test('init script function is serializable (no closures over Node APIs)', async () => {