906 Commits

Author SHA1 Message Date
SGavrl 13198fd66e Merge pull request #632 from xrip/fix/binding-called-session
fix(cdp): deliver Runtime.bindingCalled to the session that subscribed
2026-08-19 19:16:08 +00:00
SGavrl 44207a4e73 Merge pull request #587 from mnaza/fix/js-pbkdf2-cap-iterations-580
fix(js): cap PBKDF2 iterations and output length to prevent runtime DoS
2026-08-19 19:14:13 +00:00
SGavrl b836372c6c Merge pull request #566 from lisa0314/docs/531-module-budget
docs: document ES module timeout budgets
2026-08-19 19:11:46 +00:00
SGavrl 8c1f460f31 Merge pull request #670 from aech/fix/document-write-input-stream
fix(js): document.write feeds one input stream and inserts at the insertion point
2026-08-19 18:10:30 +00:00
SGavrl ba7fa94aa2 Merge pull request #652 from xrip/upstream-fix-spa-virtual-url
fix(cdp): report same-document SPA routes after clicks
2026-08-19 18:03:06 +00:00
SGavrl 0a8288652c Merge pull request #646 from lisa0314/fix/641-outset-shadow-hole
fix(render): clip outset box shadows
2026-08-19 17:58:42 +00:00
SGavrl f4ebf3cd41 Merge pull request #645 from lisa0314/fix/642-obey-robots
fix(cli): enforce --obey-robots
2026-08-19 17:57:13 +00:00
SGavrl e5cc0d33fa Merge pull request #627 from xrip/fix/accept-language-upstream
fix(net): allow Accept-Language override
2026-08-19 17:56:10 +00:00
SGavrl 83f9c3b439 Merge pull request #625 from lisa0314/fix/599-domstringmap-global
fix(js): expose DOMStringMap globally
2026-08-19 17:50:53 +00:00
SGavrl 9c9e20d6f3 Merge pull request #611 from xrip/fix/create-event-not-supported
fix(js): reject unknown createEvent interfaces
2026-08-19 17:49:43 +00:00
SGavrl 94b6134a65 Merge pull request #589 from mnaza/fix/dom-cap-children-ancestors-walk-582
fix(dom): bound children() and ancestors() walks against cyclic chains
2026-08-19 17:49:06 +00:00
SGavrl 805538e6bc Merge pull request #586 from mnaza/fix/cdp-setfileinputfiles-allow-file-access-579
fix(cdp): gate DOM.setFileInputFiles behind --allow-file-access
2026-08-19 17:43:21 +00:00
SGavrl e6d8152352 Merge pull request #585 from mnaza/fix/cdp-removebinding-validate-name-578
fix(cdp): validate Runtime.removeBinding name to prevent JS injection
2026-08-19 17:32:52 +00:00
SGavrl 840b1a5720 Merge pull request #568 from Zed128/fix-482-stealth-dump-original
fix(cli): apply `--stealth` to `--dump original` fetches
2026-08-19 17:29:28 +00:00
SGavrl 513d127838 Merge pull request #560 from mnaza/fix/cdp-objectid-backslash-escape-557
fix(cdp): escape backslashes in objectId JS interpolation
2026-08-19 17:28:44 +00:00
SGavrl 9e326ed1e3 Merge pull request #545 from zhangyang-crazy-one/fix/domparser-xml-parsererror
fix(js): DOMParser XML mime types produce parsererror on malformed XML
2026-08-19 17:27:19 +00:00
SGavrl 6065683a8e Merge pull request #537 from dgcavalcante-png/fix/bootstrap-iife-script-scope
fix(js): wrap bootstrap in an IIFE so DOM classes stop leaking into the shared script scope
2026-08-19 17:26:56 +00:00
aech 0ca7ac0599 fix(js): document.write feeds one input stream per document
document.write() inserts into the document's input stream. There is one
stream and one insertion point per document, and the tokenizer carries
its state across calls, so a construct may be split anywhere, even in
the middle of a tag name.

Obscura parsed every call as a standalone fragment. Anything spanning
two calls was lost and stayed in the body as text. The SAP UI5
cachebuster writes exactly that way, one call for the tag, one per
attribute, then '>' and the closing tag, so none of its bootstrap parts
were ever requested.

Obscura parses a document in one pass, so there is no live tokenizer to
join. This keeps one parser per document instead, which is what gives
the calls a shared tokenizer state. It parses into its own tree and
mirrors that into the document as it grows. Only <script> and <template>
wait until they are complete, a script because inserting it runs it, a
template because its children live in a separate contents document that
a child walk never reaches.

Which nodes are finished comes from TreeBuilder::trace_handles, not from
TreeSink::pop. pop looks like the signal, and its doc says so, but of
the three paths that pop the stack of open elements only one reports to
the sink; an end tag usually takes pop_until, which does not.

Written nodes also went to the end of the body rather than to the
insertion point, so a script in the <head> inserted behind everything
the parser had already seen. bootstrap.js already tracks the running
script in __currentScriptNid, and the point advances with every node
placed, across calls too.

The host creates the nodes without attaching them and returns where each
belongs. bootstrap.js attaches them with appendChild and insertBefore so
the insertion steps run.

The mirroring walk starts at the last child and stops at the first node
already handed over, because the parser only appends. Reading the whole
child list per call made the cost grow with the stream: 852 ms against
370 ms over 5000 calls.

Interleaved against d9ef40b, same release build, fifteen runs per
revision, 5000 operations per case, medians:

  insertBefore              168 ms -> 168 ms    0.0%
  replaceChild              299 ms -> 316 ms   +5.7%
  insertBefore w/ observer  163 ms -> 182 ms  +11.7%
  document.write            553 ms -> 557 ms   +0.7%

The observer case is the only one that adds work. It is the alignment
with appendChild, which always reported: on this branch insertBefore
with an active observer costs 165 ms, appendChild 160 ms.

document.open() drops the stream, since a fresh parse begins.
2026-08-16 12:15:04 +02:00
aech 5177304597 fix(js): insertBefore and replaceChild run the full insertion steps
appendChild and removeChild report a childList mutation, and appendChild
loads a written stylesheet. insertBefore and replaceChild did neither,
so an observer on a parent saw a node appear only when it went to the
end, and a <link> placed anywhere else never loaded.

before(), after() and replaceWith() all route through insertBefore, so
they were affected too. The new test walks those plus replaceChild on
connected elements, which is the check AGENTS.md asks for after touching
a mutation method. Argument order was correct everywhere, only the
reporting was missing.
2026-08-16 11:44:20 +02:00
SGavrl d9ef40bea2 Merge pull request #665 from aech/fix/fetch-redirect-limit-20
fetch follows twenty redirects, as the Fetch standard requires
2026-08-15 18:48:25 +00:00
Andrey Mnatsakanov 01bc142086 fix(dom): bound children() and ancestors() walks against cyclic chains
descendants() already caps its walk at nodes.len() as defense-in-depth
against a corrupted/cyclic node graph, but children() (sibling chain) and
ancestors() (parent chain) looped over next_sibling / parent pointers
with no bound. A single corrupted pointer would hang these walkers — and
every caller — forever, while descendants() would recover.

The append_child / insert_before guards prevent such cycles through the
public API, so this is hardening, not a reachable bug. Mirror the
descendants() bound in both loops: stop once the collected count exceeds
nodes.len().

Adds tests that forge a sibling cycle and a parent cycle by writing the
node arena directly and assert each walk stays bounded instead of
hanging.

Closes #582
2026-08-15 18:35:09 +02:00
Andrey Mnatsakanov cfda91b3c7 fix(js): cap PBKDF2 iterations and output length to prevent runtime DoS
crypto.subtle.deriveBits/deriveKey for PBKDF2 passed the iteration count
and output length from page JS straight into op_subtle_pbkdf2 with no
bound. Because the JS runtime is single-threaded and shared across the
CDP connection, a page calling deriveBits with iterations=4294967295
pins the V8 isolate for hours and blocks every other command; a huge
requested length forces an unbounded vec![0u8; length] allocation.

Split the derivation into a testable pbkdf2_derive helper that rejects
iteration counts above 10_000_000 and output lengths above 1 MiB — both
far above any legitimate use (OWASP recommends ~600k iterations; derived
keys are tens of bytes) — with an OperationError before doing the work.

Closes #580
2026-08-15 18:35:08 +02:00
Andrey Mnatsakanov c6d96453da fix(cdp): gate DOM.setFileInputFiles behind --allow-file-access
DOM.setFileInputFiles read client-supplied local file paths from disk
and handed their bytes to page JS with no access control. Any client
that can reach the CDP port (default localhost, but Docker images bind
0.0.0.0) could thus read any file the process can read — e.g.
setFileInputFiles with files=["/etc/passwd"].

Page.navigate to file:// already guards this exact threat behind
context.allow_file_access (off by default; opt in with
`obscura serve --allow-file-access`). Apply the same gate here, refusing
with a matching error before any std::fs::read when the flag is off.

Adds a test proving the handler refuses to read an existing file when
allow_file_access is off.

Closes #579
2026-08-15 18:35:08 +02:00
Andrey Mnatsakanov 7a6ca01d4f fix(cdp): validate Runtime.removeBinding name to prevent JS injection
Runtime.removeBinding interpolated the client-supplied binding name
directly into `delete globalThis['{name}']` with no validation, so a
CDP client could break out of the string literal and execute arbitrary
JS in the page context — e.g. name = "x'] , (evil()) , globalThis['y".

Runtime.addBinding already guarded against this by requiring the name to
be a plain identifier. Extract that check into a shared
`is_valid_binding_name` helper and gate removeBinding behind it too; a
non-identifier name is silently ignored (there is nothing to delete).

Adds an end-to-end test that drives removeBinding against a live page
with a comma-expression payload and asserts the injected assignment
never runs.

Closes #578
2026-08-15 18:35:08 +02:00
SGavrl 95d1706eae Merge pull request #668 from aech/fix/frame-wrappers-borrow-the-page
The frame wrappers borrow the page, otherwise main does not build
2026-08-15 15:42:30 +00:00
aech bcf58af77f fix(obscura): the frame wrappers borrow the page
Two lines in crates/obscura/src/page.rs call methods directly on
self.inner, which is now a RefCell<Page>. So main does not build:

  error[E0599]: no method named `frame_urls` found for struct `RefCell<T>`
  error[E0599]: no method named `evaluate_in_frame` found for struct `RefCell<T>`

It is a conflict between two changes that were each correct on their
own. One introduced the two wrappers, another turned Page.inner into
a RefCell. Both ran green against the main of their time. The merge
does not see it, because no textual conflict arises.

The neighbor shows how it was meant: evaluate directly above already
borrows. The two frame wrappers were overlooked during the rework.

frame_urls reads, so borrow(). evaluate_in_frame takes &mut self,
so borrow_mut().

No new tests. crates/obscura/tests/child_frame_scripts.rs already
uses both methods in 14 places, and it just would not
build as
long as the library does not build.
2026-08-15 12:49:41 +02:00
aech 4b90ec38bf fix(js): fetch follows twenty redirects, as the standard requires
HTTP-redirect fetch returns a network error as soon as a request's
redirect count reaches 20, and only increments the counter
afterwards. The twentieth hop must still succeed and the
twenty-first must fail. FETCH_REDIRECT_LIMIT was 10, so fetch() and
XMLHttpRequest gave up after half the redirects the standard allows.

https://fetch.spec.whatwg.org/#http-redirect-fetch

WPT covers both ends of the pair in
fetch/api/redirect/redirect-count.any.js: twenty hops arrive,
twenty-one are rejected. New tests against a local redirect chain
cover both.

The comment claimed the value matched the reqwest default. It does
not apply here: the redirects are followed by hand in ops.rs, one
hop per loop iteration, so that each hop is checked against the
SSRF rules again.
2026-08-14 21:45:29 +02:00
SGavrl 8ceac62000 Merge pull request #590 from mnaza/fix/obscura-escape-attribute-name-583
fix(obscura): escape Element::attribute() name and make Element borrow Page safely
2026-08-14 18:59:26 +00:00
SGavrl 13b785222f Merge pull request #637 from xrip/fix/iframe-lifecycle-cleanup-600
fix(frames): release iframe realms safely
2026-08-14 18:54:51 +00:00
SGavrl 3a6b7afc15 Merge pull request #660 from h4ckf0r0day/ci/fix-pr-check-scope
ci: keep candidate artifact stable across reruns
2026-08-14 12:29:15 +00:00
SGavrl 8acd973d62 ci: keep candidate artifact stable across reruns 2026-08-14 12:27:11 +00:00
SGavrl ed2bb73c43 Merge pull request #659 from h4ckf0r0day/ci/fix-pr-check-scope
ci: compare obstacle failures against base
2026-08-14 12:24:00 +00:00
SGavrl ea84c60ed9 ci: compare obstacle failures against base 2026-08-14 11:51:44 +00:00
SGavrl 36b6de79ab Merge pull request #657 from h4ckf0r0day/ci/fix-pr-check-scope
ci: run checks only for pull requests
2026-08-14 10:54:45 +00:00
SGavrl 0caa646f04 ci: run checks only for pull requests 2026-08-14 10:53:36 +00:00
SGavrl 9ac65da7d4 Merge pull request #656 from h4ckf0r0day/ci/fix-pr-check-scope
ci: stop enforcing PR description headings
2026-08-14 10:48:38 +00:00
SGavrl d9a86479ac ci: stop enforcing PR description headings 2026-08-14 10:46:46 +00:00
SGavrl 923bee0053 Merge pull request #655 from h4ckf0r0day/ci/fix-pr-check-scope
ci: scope pull request checks
2026-08-14 10:45:37 +00:00
SGavrl afa95ab6e1 ci: scope pull request checks 2026-08-14 10:38:21 +00:00
h4ckf0r0day f458a7f23d Update README.md 2026-08-14 03:16:35 +02:00
h4ckf0r0day fcd2211d40 Update README.md 2026-08-14 03:11:06 +02:00
SGavrl 28e230cd0b Merge pull request #654 from h4ckf0r0day/ci/harden-pr-checks
ci: harden pull request validation
2026-08-14 00:10:38 +00:00
SGavrl c4124e118e ci: add repository owner to CODEOWNERS 2026-08-14 00:04:57 +00:00
SGavrl 24306be4f3 ci: harden pull request validation 2026-08-14 00:00:39 +00:00
xrip 6b6ad930c1 fix(cdp): report same-document SPA routes after clicks 2026-08-13 12:08:28 +03:00
lisa0314 00a8a8111e fix(render): clip outset box shadows 2026-08-12 11:52:30 +08:00
lisa0314 92a70fe69f fix(cli): enforce --obey-robots 2026-08-12 11:22:41 +08:00
SGavrl 94374e638c Merge pull request #639 from marcoripa96/fix/638-select-semantics
fix(js): HTMLSelectElement parity: type, change on assignment, selectedIndex
2026-08-11 16:08:03 +00:00
SGavrl 57a6394751 Merge pull request #620 from xrip/fix/stealth-get-reset-retry
fix(net): retry GET after connection reset
2026-08-11 16:07:38 +00:00
SGavrl d10381a381 Merge pull request #615 from xrip/fix/storage-clear-cookies
fix(cdp): implement Storage.clearCookies
2026-08-11 16:05:00 +00:00