mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
feat(humanize): implement CDP Isolated Worlds and trusted keyboard events (fixes #110)
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
# 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()
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user