Skip to content

Commit c21717a

Browse files
authored
Merge pull request #96 from VectorlyApp/js-looser-blocker
Js looser blocker
2 parents bef55f9 + 7011bd9 commit c21717a

File tree

5 files changed

+44
-44
lines changed

5 files changed

+44
-44
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
66

77
[project]
88
name = "web-hacker"
9-
version = "1.2.3"
9+
version = "1.3"
1010
description = "SDK for reverse engineering web apps"
1111
readme = "README.md"
1212
requires-python = ">=3.12.3,<3.13"

tests/unit/test_operations.py

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,20 @@ def test_blocked_fetch_in_async_iife(self) -> None:
237237
errors = exc_info.value.errors()
238238
assert any("fetch" in str(e.get("msg", "")) for e in errors)
239239

240+
def test_allowed_prefetch(self) -> None:
241+
"""Test that prefetch() is not blocked by the fetch pattern."""
242+
operation = RoutineJsEvaluateOperation(
243+
js="(function() { var link = document.createElement('link'); link.rel = 'prefetch'; return prefetch('/next-page'); })()"
244+
)
245+
assert operation.js is not None
246+
247+
def test_allowed_refetch(self) -> None:
248+
"""Test that refetch() is not blocked by the fetch pattern."""
249+
operation = RoutineJsEvaluateOperation(
250+
js="(function() { return refetch(); })()"
251+
)
252+
assert operation.js is not None
253+
240254
def test_blocked_eval_in_async_iife(self) -> None:
241255
"""Test that eval() is blocked even in async IIFE."""
242256
with pytest.raises(ValidationError) as exc_info:
@@ -287,16 +301,6 @@ def test_blocked_addeventlistener(self) -> None:
287301
errors = exc_info.value.errors()
288302
assert any("addEventListener" in str(e.get("msg", "")) for e in errors)
289303

290-
def test_blocked_onevent_handler(self) -> None:
291-
"""Test that onclick= style handlers are blocked."""
292-
with pytest.raises(ValidationError) as exc_info:
293-
RoutineJsEvaluateOperation(
294-
js="(function() { document.onclick = () => {}; })()"
295-
)
296-
297-
errors = exc_info.value.errors()
298-
assert any("on" in str(e.get("msg", "")) for e in errors)
299-
300304
def test_blocked_mutation_observer(self) -> None:
301305
"""Test that MutationObserver is blocked."""
302306
with pytest.raises(ValidationError) as exc_info:
@@ -327,26 +331,6 @@ def test_blocked_window_close(self) -> None:
327331
errors = exc_info.value.errors()
328332
assert any("window\\.close" in str(e.get("msg", "")) or "window.close" in str(e.get("msg", "")) for e in errors)
329333

330-
def test_blocked_location(self) -> None:
331-
"""Test that location.* is blocked."""
332-
with pytest.raises(ValidationError) as exc_info:
333-
RoutineJsEvaluateOperation(
334-
js="(function() { location.href = 'http://example.com'; })()"
335-
)
336-
337-
errors = exc_info.value.errors()
338-
assert any("location" in str(e.get("msg", "")) for e in errors)
339-
340-
def test_blocked_history(self) -> None:
341-
"""Test that history.* is blocked."""
342-
with pytest.raises(ValidationError) as exc_info:
343-
RoutineJsEvaluateOperation(
344-
js="(function() { history.pushState({}, '', '/new'); })()"
345-
)
346-
347-
errors = exc_info.value.errors()
348-
assert any("history" in str(e.get("msg", "")) for e in errors)
349-
350334
def test_allowed_promise(self) -> None:
351335
"""Test that Promise is allowed."""
352336
operation = RoutineJsEvaluateOperation(
@@ -660,6 +644,28 @@ def test_post_interpolation_validation_allows_safe_interpolation(self) -> None:
660644
# Complex Real-World Examples
661645
# ============================================================================
662646

647+
def test_allowed_string_containing_on_prefix(self) -> None:
648+
"""Test that string literals containing 'on' followed by word chars and '=' are not blocked.
649+
650+
The pattern r'on\\w+\\s*=' is meant to block event handlers like onclick=, onload=, etc.
651+
But it should NOT block occurrences inside string literals (e.g., 'lots_json_length=').
652+
"""
653+
js_code = (
654+
"(function () { var r = window.chrComponents && window.chrComponents.lots; "
655+
"if (!(r && r.data && Array.isArray(r.data.lots) && r.data.lots.length)) { "
656+
"console.log('no_lots'); return null; } var a = r.data.lots; var o = []; "
657+
"for (var i = 0; i < a.length; i++) { var l = a[i]; o.push({ "
658+
"lot_number: l.lot_id_txt || null, artist: l.title_primary_txt || null, "
659+
"title: l.title_secondary_txt || null, estimate_text: l.estimate_txt || null, "
660+
"estimate_low: l.estimate_low || null, estimate_high: l.estimate_high || null, "
661+
"price_realised_text: l.price_realised_txt || null, price_realised: l.price_realised || null, "
662+
"url: l.url || null }); } var s = JSON.stringify(o); "
663+
"console.log('lots_json_length=' + s.length); "
664+
"sessionStorage.setItem('lots_json', s); return null; })()"
665+
)
666+
operation = RoutineJsEvaluateOperation(js=js_code)
667+
assert operation.js == js_code
668+
663669
def test_complex_valid_code(self) -> None:
664670
"""Test complex but valid JS code."""
665671
js_code = """(function() {

web_hacker/agent_docs/common-issues/js-evaluate-issues.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ Check `console_logs` in operation metadata.
8686
**Cause:** Security restrictions block certain APIs.
8787
8888
**Blocked patterns:**
89-
- `fetch()` → Use `fetch` operation instead
89+
- `fetch()` → Use `fetch` operation instead (note: `prefetch()`, `refetch()` etc. are allowed)
9090
- `eval()`, `Function()` → Rewrite without dynamic code
91-
- `addEventListener()` → Not supported
92-
- `location`, `history` → Use `navigate` operation
91+
- `addEventListener()`, `MutationObserver`, `IntersectionObserver` → Not supported
92+
- `window.close()` → Not supported

web_hacker/agent_docs/operations/js-evaluate.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,17 +212,14 @@ These patterns are detected and rejected:
212212
|---------|--------|
213213
| `eval()` | Dynamic code generation |
214214
| `Function()` constructor | Dynamic code generation |
215-
| `fetch()` | Use `fetch` operation instead |
215+
| `fetch()` | Use `fetch` operation instead (note: `prefetch()`, `refetch()` etc. are allowed) |
216216
| `XMLHttpRequest` | Network requests |
217217
| `WebSocket` | Network requests |
218218
| `sendBeacon` | Exfiltration |
219219
| `addEventListener()` | Persistent event hooks |
220-
| `on*=` handlers | Persistent event hooks |
221220
| `MutationObserver` | Persistent observers |
222221
| `IntersectionObserver` | Persistent observers |
223222
| `window.close()` | Lifecycle control |
224-
| `location.*` | Navigation |
225-
| `history.*` | Navigation |
226223
227224
---
228225

web_hacker/data_models/routine/operation.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,8 +1004,8 @@ class RoutineJsEvaluateOperation(RoutineOperation):
10041004
BLOCKED:
10051005
- Dynamic code generation: eval(), Function constructor
10061006
- Network requests: fetch(), XMLHttpRequest, WebSocket, sendBeacon (use RoutineFetchOperation instead)
1007-
- Persistent event hooks: addEventListener(), on*=, MutationObserver, IntersectionObserver
1008-
- Navigation/lifecycle: window.close(), location.*, history.*
1007+
- Persistent event hooks: addEventListener(), MutationObserver, IntersectionObserver
1008+
- Navigation/lifecycle: window.close()
10091009
10101010
FORMAT REQUIREMENT:
10111011
The JavaScript code MUST be wrapped in an IIFE (Immediately Invoked Function Expression):
@@ -1043,21 +1043,18 @@ class RoutineJsEvaluateOperation(RoutineOperation):
10431043
r'(?:^|[^a-zA-Z0-9_])Function\s*\(',
10441044

10451045
# Network / exfiltration
1046-
r'fetch\s*\(',
1046+
r'(?<![a-zA-Z0-9_])fetch\s*\(',
10471047
r'XMLHttpRequest',
10481048
r'WebSocket',
10491049
r'sendBeacon',
10501050

10511051
# Persistent event hooks
10521052
r'addEventListener\s*\(',
1053-
r'on\w+\s*=',
10541053
r'MutationObserver',
10551054
r'IntersectionObserver',
10561055

10571056
# Navigation / lifecycle control
10581057
r'window\.close\s*\(',
1059-
r'location\.',
1060-
r'history\.',
10611058
]
10621059

10631060
@field_validator("js")

0 commit comments

Comments
 (0)