Short version: BrewShot hands your input to Chromium and reads back bytes and JSON — it never interprets page content itself. A malicious page is Chromium's threat model, not BrewShot's. The real risks are the ordinary ones that come with driving a browser, listed at the end.
BrewShot launches your local Chrome, speaks the DevTools Protocol over a WebSocket, and consumes exactly three things back from the page:
| From the page | BrewShot does | Risk |
|---|---|---|
| screenshot | Base64.decode → Files.write |
opaque bytes, never decoded/parsed by us |
eval result |
MiniJson.parse → typed value |
the one place page data enters Java — see below |
| CDP events | routed by method name; console/errors kept as text | bounded, never executed |
The Java code and the page's HTML/JS never touch. There is no place where
page markup or scripts are interpreted, eval'd, deserialized into objects, or
reflected into class loading on the Java side. A .html crafted to compromise
the Java process would first have to break Chromium's renderer sandbox —
a browser-exploit-chain problem entirely inside Chrome's threat model.
eval results arrive as JSON and are parsed by the hand-rolled MiniJson.
This is the only path where attacker-influenced data reaches Java, so it is
hardened accordingly:
- Depth-capped (
MAX_DEPTH = 200): a pathologically nested value fails the singleevalcall with a clearIllegalArgumentException, never aStackOverflowErrorthat takes down the harness. - Malformed input fails closed: truncated strings, bad
\uescapes, trailing garbage → a caughtIllegalArgumentException, not undefined behaviour. (Pinned byMiniJsonTest.) - No object mapping: it produces only
Map/List/String/Double/Boolean/null— no reflection, no type coercion, no gadget surface. There is nothing for a crafted payload to instantiate.
Worst case from a hostile eval result today: your eval/clip-js call
throws. No code execution, no process compromise.
These are inherent to any headless-browser tool. BrewShot is a test/CI/agent harness for TRUSTED pages driven by a trusted operator — calibrate to that.
-
eval/--fail-js/--clip-jsrun JavaScript you supply, in the page's context. That is the feature. But if you build that JS string by concatenating untrusted input, you have injected into your own probe. Keepevalexpressions static, or escape anything interpolated. -
SSRF-style reach. Point BrewShot at a URL and your machine/network fetches it — same as
curlor any headless browser. Safe for the intendedlocalhost/your-own-pages use; if a target URL could ever come from an untrusted source, treat it like any server-side fetch (allowlist hosts, run it network-isolated). -
--no-sandboxin containers. The provided Docker image sets--no-sandboxbecause Chrome's sandbox needs privileges containers don't grant by default. That removes the layer that would contain a malicious renderer, so only use the sandboxless container to render pages you trust — never point a sandboxless browser at untrusted URLs. (The image also runs as a non-root user, which limits the blast radius if the renderer is compromised.) -
header()is not host-scoped. An extra header (e.g.Authorization) is sent on every request the page makes, including cross-origin subresources — a credential can leak off-host if the page pulls from elsewhere. For host-scoped credentials prefercookie(), which the browser applies under its own same-domain rules.
This is a small, single-maintainer utility provided under Apache-2.0 on an "AS IS" basis (see LICENSE). If you find a genuine Java-side memory-safety or code-execution issue (as opposed to a Chromium bug — report those upstream to Chrome), open an issue describing the input and observed behaviour.