A browser smoke test sounds read-only: open a page, inspect the DOM, save a
report.
But the page controls much of what the browser does next. It can request another
host, attempt a POST, open a WebSocket, register a service worker, or use a
hostname that resolves somewhere very different from what its text suggests.
That changed the design of a small tool I was building. I wanted a release gate
that could inspect public websites without quietly becoming a general-purpose
network client.
The result is PARSAI Release Gate,
an MIT-licensed Playwright CLI for at most two public routes and two viewports.
The small scope is deliberate: the goal is a reproducible release decision, not
a crawler.
The tempting version was not enough
The first design most of us reach for looks roughly like this:
- parse the configured origin;
- reject
localhostand obvious private IP literals; - let Playwright navigate; and
- intercept requests that do not look safe.
Each step helps. Together, they still leave gaps.
A hostname that looks public can resolve to 127.0.0.1, an RFC1918 address, or
another reserved range. Even if the first lookup is safe, a later lookup can
return a different address. Alternate IPv4 forms such as 2130706433 and
0x7f000001 also represent loopback and are easy to miss with string matching.
Request interception has a similar problem: it is useful application policy,
but it should not be the only egress boundary. The browser has more than normal
page fetches, including WebSockets, service workers, WebRTC, QUIC, and
WebTransport.
So I made the network boundary part of the release gate itself.
1. Resolve once, then classify every address
For each hostname, the gate performs one bounded lookup and caches the result.
It rejects private, loopback, link-local, multicast, documentation, translated,
and other reserved address forms unless the operator explicitly enables local
testing.
It also limits the run to 64 resolved hosts and 16 addresses per host. Those
limits keep a two-page audit from turning into unbounded DNS fan-out.
The important distinction is that the policy returns the approved address set.
It does not merely return true and let the browser resolve the hostname again.
2. Pin Chromium to the approved result
The gate starts a local authenticated proxy for each route-and-viewport case.
HTTP and CONNECT traffic is forwarded only to an address returned by the network
policy.
Chromium receives the normal hostname for TLS and origin behavior, while the
proxy owns the actual upstream connection. That closes the gap between “the DNS
check passed” and “the browser connected somewhere else.”
The proxy is not advertised as a hardened sandbox. Operating-system egress
controls remain the stronger boundary against an actively malicious target. It
is, however, an observable and testable improvement over trusting a second DNS
lookup.
3. Block side effects before target contact
The browser context allows only GET, HEAD, and OPTIONS. A page that attempts
a mutating method gets a local empty response, and the attempt is recorded in
the JSON evidence without contacting the target.
WebSockets are closed with a policy error. Service workers are blocked. Chromium
is launched without QUIC and non-proxied WebRTC, and page-level WebRTC and
WebTransport constructors are disabled as an additional guard.
This produced a useful testing question: should a prevented side-effect attempt
fail the page?
For this bounded static gate, the answer is no by itself. The gate records the
attempt, then still evaluates the rendered page. If blocking it causes a page or
console error, that error fails normally. The report distinguishes “the page
tried this and the gate stopped it” from “the observed release case is broken.”
4. Test the boundary from the server side
It is easy to assert that an interceptor ran. I wanted evidence that the unsafe
requests never arrived.
The integration fixture serves a page that attempts both a POST and a
WebSocket connection. The test keeps counters on the server, runs the audit, and
then checks all three facts:
assert.equal(postRequests, 0);
assert.equal(websocketUpgrades, 0);
assert.ok(evidence.some(({ kind }) => kind === "websocket"));
Other tests cover alternate loopback forms, private and translated IPv6 ranges,
DNS fan-out, standard public ports, output-directory escape through a junction,
and removal of credentials, query strings, and fragments from report URLs.
The current release has 21 passing tests. Its self-initiated example audit ran
two PARSAI public routes at desktop and mobile sizes and passed all four cases.
That is product verification, not a client result or a claim that the audited
site has no defects.
What the release decision actually checks
For every route × viewport case, the gate records:
- navigation status and final origin;
- one visible
h1and at least one visiblemain; - completed images with valid natural dimensions;
- horizontal overflow beyond one pixel;
- same-origin console errors, page errors, and failed requests;
- private or reserved network attempts;
- whether the authenticated pinned proxy was observed; and
- any prevented mutating request or WebSocket attempt.
It writes machine-readable JSON and a short Markdown report. Exit code 0 means
all cases passed, 1 means the audit ran and a case failed, and 2 means setup
or execution failed.
Try it
You need Node.js 22, 24, or 26 on a Playwright-supported operating system:
git clone https://github.com/ParsaiWorks/parsai-release-gate.git
cd parsai-release-gate
npm ci
npm run install:browser
npm run audit
Edit the data-only JSON config to use your own public origin and one or two
routes. Private hosts remain off unless you pass the explicit local-testing
flag from the command line.
Deliberate limits
This is not WCAG conformance testing, penetration testing, load testing,
exhaustive browser coverage, authenticated-flow testing, or proof that no defect
exists. It does not submit forms, execute payments, or modify the target.
That boundary is part of the product. A small release gate is more useful when
its claims are as constrained as its inputs.
Commercial note: the repository is free and MIT-licensed. I also offer a
fixed-scope Production Web QA Audit
for up to eight public routes, two viewports, agreed checks, evidence screenshots,
and a concise report. It is US$300 with delivery in two business days after
complete inputs and written scope confirmation; please send the inputs first so
I can confirm fit before ordering. One bounded correction for a mismatch with
the written acceptance checklist is included for seven calendar days;
authenticated or private flows, source-code fixes, full accessibility or
security audits, and load testing are excluded.
Top comments (0)