169 lines
5.9 KiB
Python
169 lines
5.9 KiB
Python
# tests/test_stealth_reproduction_110.py
|
|
"""
|
|
Exact reproduction of issue #110 detection vectors.
|
|
Proves all three leaks (isInputElement, isSelectorFocused, typeShiftSymbol)
|
|
are fixed with CDP isolated worlds.
|
|
"""
|
|
import asyncio
|
|
import pytest
|
|
|
|
@pytest.mark.slow
|
|
class TestIssue110Reproduction:
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_exact_reproduction_from_issue(self):
|
|
"""Exact detection script from issue #110 — must produce zero detections."""
|
|
from cloakbrowser import launch_async
|
|
|
|
browser = await launch_async(headless=True, humanize=True)
|
|
page = await browser.new_page()
|
|
|
|
await page.goto('https://www.wikipedia.org', wait_until='domcontentloaded')
|
|
await asyncio.sleep(1)
|
|
|
|
# === EXACT detection from issue #110 ===
|
|
await page.evaluate("""
|
|
() => {
|
|
window.__detections = {
|
|
evaluateQS: [],
|
|
untrustedKeydown: []
|
|
};
|
|
|
|
// Detection 1: querySelector from evaluate context
|
|
const origQS = document.querySelector.bind(document);
|
|
document.querySelector = function(sel) {
|
|
try { throw new Error(); } catch (e) {
|
|
if (e.stack.includes(':302:')) {
|
|
window.__detections.evaluateQS.push(sel);
|
|
}
|
|
}
|
|
return origQS(sel);
|
|
};
|
|
|
|
// Detection 2: untrusted keyboard events
|
|
document.addEventListener('keydown', (e) => {
|
|
if (!e.isTrusted) {
|
|
window.__detections.untrustedKeydown.push(e.key);
|
|
}
|
|
}, true);
|
|
}
|
|
""")
|
|
|
|
# === Trigger all three vectors from issue ===
|
|
|
|
# Vector 1: isInputElement — click triggers querySelector check
|
|
await page.click('#searchInput')
|
|
await asyncio.sleep(0.3)
|
|
|
|
# Vector 2+3: typeShiftSymbol — type text with shift symbols
|
|
await page.keyboard.type('Hello!@#$%^&*()')
|
|
await asyncio.sleep(0.5)
|
|
|
|
# === Verify: zero detections ===
|
|
detections = await page.evaluate('() => window.__detections')
|
|
|
|
qs_leaks = detections['evaluateQS']
|
|
untrusted = detections['untrustedKeydown']
|
|
|
|
print(f"\n{'='*60}")
|
|
print(f"Issue #110 Reproduction Results:")
|
|
print(f" querySelector from evaluate: {len(qs_leaks)} detections")
|
|
print(f" Untrusted keyboard events: {len(untrusted)} detections")
|
|
print(f"{'='*60}")
|
|
|
|
assert len(qs_leaks) == 0, (
|
|
f"LEAK: querySelector called from evaluate context: {qs_leaks}"
|
|
)
|
|
assert len(untrusted) == 0, (
|
|
f"LEAK: Untrusted keyboard events detected: {untrusted}"
|
|
)
|
|
|
|
await browser.close()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_all_21_shift_symbols_trusted(self):
|
|
"""Every single shift symbol must produce isTrusted=true."""
|
|
from cloakbrowser import launch_async
|
|
|
|
browser = await launch_async(headless=True, humanize=True)
|
|
page = await browser.new_page()
|
|
|
|
await page.goto('https://www.wikipedia.org', wait_until='domcontentloaded')
|
|
await asyncio.sleep(1)
|
|
|
|
await page.evaluate("""
|
|
() => {
|
|
window.__keyResults = { trusted: [], untrusted: [] };
|
|
const input = document.querySelector('#searchInput');
|
|
input.addEventListener('keydown', (e) => {
|
|
const list = e.isTrusted ? 'trusted' : 'untrusted';
|
|
window.__keyResults[list].push(e.key);
|
|
}, true);
|
|
}
|
|
""")
|
|
|
|
await page.click('#searchInput')
|
|
await asyncio.sleep(0.3)
|
|
|
|
# Type ALL 21 shift symbols
|
|
all_shift = '!@#$%^&*()_+{}|:"<>?~'
|
|
await page.keyboard.type(all_shift)
|
|
await asyncio.sleep(1)
|
|
|
|
results = await page.evaluate('() => window.__keyResults')
|
|
|
|
print(f"\n{'='*60}")
|
|
print(f"All 21 Shift Symbols Test:")
|
|
print(f" Trusted: {results['trusted']}")
|
|
print(f" Untrusted: {results['untrusted']}")
|
|
print(f"{'='*60}")
|
|
|
|
# Every shift symbol must be trusted
|
|
for sym in all_shift:
|
|
assert sym in results['trusted'], f"'{sym}' NOT in trusted events"
|
|
assert sym not in results['untrusted'], f"'{sym}' IS in untrusted events"
|
|
|
|
await browser.close()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clear_uses_isolated_world(self):
|
|
"""clear() calls isSelectorFocused — must not leak evaluate."""
|
|
from cloakbrowser import launch_async
|
|
|
|
browser = await launch_async(headless=True, humanize=True)
|
|
page = await browser.new_page()
|
|
|
|
await page.goto('https://www.wikipedia.org', wait_until='domcontentloaded')
|
|
await asyncio.sleep(1)
|
|
|
|
await page.evaluate("""
|
|
() => {
|
|
window.__evalLeaks = [];
|
|
const origQS = document.querySelector.bind(document);
|
|
document.querySelector = function(sel) {
|
|
try { throw new Error(); } catch (e) {
|
|
if (e.stack.includes(':302:')) {
|
|
window.__evalLeaks.push(sel);
|
|
}
|
|
}
|
|
return origQS(sel);
|
|
};
|
|
}
|
|
""")
|
|
|
|
# fill → click + type (isInputElement + isSelectorFocused)
|
|
await page.locator('#searchInput').fill('some text')
|
|
await asyncio.sleep(0.3)
|
|
|
|
# clear → isSelectorFocused check
|
|
await page.locator('#searchInput').clear()
|
|
await asyncio.sleep(0.3)
|
|
|
|
leaks = await page.evaluate('() => window.__evalLeaks')
|
|
assert len(leaks) == 0, f"clear() leaked via evaluate: {leaks}"
|
|
|
|
val = await page.locator('#searchInput').input_value()
|
|
assert val == '', f"clear() didn't clear: '{val}'"
|
|
|
|
await browser.close()
|