<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Parsa Hedayatmehr</title>
    <description>The latest articles on DEV Community by Parsa Hedayatmehr (@parsai).</description>
    <link>https://dev.to/parsai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4092842%2Fd04f2c05-f23d-40a5-a67d-d15ad44ad8a9.png</url>
      <title>DEV Community: Parsa Hedayatmehr</title>
      <link>https://dev.to/parsai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/parsai"/>
    <language>en</language>
    <item>
      <title>The URL Looked Public. Playwright Could Still Reach localhost.</title>
      <dc:creator>Parsa Hedayatmehr</dc:creator>
      <pubDate>Mon, 24 Aug 2026 19:02:48 +0000</pubDate>
      <link>https://dev.to/parsai/the-url-looked-public-playwright-could-still-reach-localhost-50em</link>
      <guid>https://dev.to/parsai/the-url-looked-public-playwright-could-still-reach-localhost-50em</guid>
      <description>&lt;p&gt;A browser smoke test sounds read-only: open a page, inspect the DOM, save a&lt;br&gt;
report.&lt;/p&gt;

&lt;p&gt;But the page controls much of what the browser does next. It can request another&lt;br&gt;
host, attempt a &lt;code&gt;POST&lt;/code&gt;, open a WebSocket, register a service worker, or use a&lt;br&gt;
hostname that resolves somewhere very different from what its text suggests.&lt;/p&gt;

&lt;p&gt;That changed the design of a small tool I was building. I wanted a release gate&lt;br&gt;
that could inspect public websites without quietly becoming a general-purpose&lt;br&gt;
network client.&lt;/p&gt;

&lt;p&gt;The result is &lt;a href="https://github.com/ParsaiWorks/parsai-release-gate" rel="noopener noreferrer"&gt;PARSAI Release Gate&lt;/a&gt;,&lt;br&gt;
an MIT-licensed Playwright CLI for at most two public routes and two viewports.&lt;br&gt;
The small scope is deliberate: the goal is a reproducible release decision, not&lt;br&gt;
a crawler.&lt;/p&gt;
&lt;h2&gt;
  
  
  The tempting version was not enough
&lt;/h2&gt;

&lt;p&gt;The first design most of us reach for looks roughly like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;parse the configured origin;&lt;/li&gt;
&lt;li&gt;reject &lt;code&gt;localhost&lt;/code&gt; and obvious private IP literals;&lt;/li&gt;
&lt;li&gt;let Playwright navigate; and&lt;/li&gt;
&lt;li&gt;intercept requests that do not look safe.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step helps. Together, they still leave gaps.&lt;/p&gt;

&lt;p&gt;A hostname that looks public can resolve to &lt;code&gt;127.0.0.1&lt;/code&gt;, an RFC1918 address, or&lt;br&gt;
another reserved range. Even if the first lookup is safe, a later lookup can&lt;br&gt;
return a different address. Alternate IPv4 forms such as &lt;code&gt;2130706433&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;0x7f000001&lt;/code&gt; also represent loopback and are easy to miss with string matching.&lt;/p&gt;

&lt;p&gt;Request interception has a similar problem: it is useful application policy,&lt;br&gt;
but it should not be the only egress boundary. The browser has more than normal&lt;br&gt;
page fetches, including WebSockets, service workers, WebRTC, QUIC, and&lt;br&gt;
WebTransport.&lt;/p&gt;

&lt;p&gt;So I made the network boundary part of the release gate itself.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Resolve once, then classify every address
&lt;/h2&gt;

&lt;p&gt;For each hostname, the gate performs one bounded lookup and caches the result.&lt;br&gt;
It rejects private, loopback, link-local, multicast, documentation, translated,&lt;br&gt;
and other reserved address forms unless the operator explicitly enables local&lt;br&gt;
testing.&lt;/p&gt;

&lt;p&gt;It also limits the run to 64 resolved hosts and 16 addresses per host. Those&lt;br&gt;
limits keep a two-page audit from turning into unbounded DNS fan-out.&lt;/p&gt;

&lt;p&gt;The important distinction is that the policy returns the approved address set.&lt;br&gt;
It does not merely return &lt;code&gt;true&lt;/code&gt; and let the browser resolve the hostname again.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Pin Chromium to the approved result
&lt;/h2&gt;

&lt;p&gt;The gate starts a local authenticated proxy for each route-and-viewport case.&lt;br&gt;
HTTP and CONNECT traffic is forwarded only to an address returned by the network&lt;br&gt;
policy.&lt;/p&gt;

&lt;p&gt;Chromium receives the normal hostname for TLS and origin behavior, while the&lt;br&gt;
proxy owns the actual upstream connection. That closes the gap between “the DNS&lt;br&gt;
check passed” and “the browser connected somewhere else.”&lt;/p&gt;

&lt;p&gt;The proxy is not advertised as a hardened sandbox. Operating-system egress&lt;br&gt;
controls remain the stronger boundary against an actively malicious target. It&lt;br&gt;
is, however, an observable and testable improvement over trusting a second DNS&lt;br&gt;
lookup.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Block side effects before target contact
&lt;/h2&gt;

&lt;p&gt;The browser context allows only &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;HEAD&lt;/code&gt;, and &lt;code&gt;OPTIONS&lt;/code&gt;. A page that attempts&lt;br&gt;
a mutating method gets a local empty response, and the attempt is recorded in&lt;br&gt;
the JSON evidence without contacting the target.&lt;/p&gt;

&lt;p&gt;WebSockets are closed with a policy error. Service workers are blocked. Chromium&lt;br&gt;
is launched without QUIC and non-proxied WebRTC, and page-level WebRTC and&lt;br&gt;
WebTransport constructors are disabled as an additional guard.&lt;/p&gt;

&lt;p&gt;This produced a useful testing question: should a prevented side-effect attempt&lt;br&gt;
fail the page?&lt;/p&gt;

&lt;p&gt;For this bounded static gate, the answer is no by itself. The gate records the&lt;br&gt;
attempt, then still evaluates the rendered page. If blocking it causes a page or&lt;br&gt;
console error, that error fails normally. The report distinguishes “the page&lt;br&gt;
tried this and the gate stopped it” from “the observed release case is broken.”&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Test the boundary from the server side
&lt;/h2&gt;

&lt;p&gt;It is easy to assert that an interceptor ran. I wanted evidence that the unsafe&lt;br&gt;
requests never arrived.&lt;/p&gt;

&lt;p&gt;The integration fixture serves a page that attempts both a &lt;code&gt;POST&lt;/code&gt; and a&lt;br&gt;
WebSocket connection. The test keeps counters on the server, runs the audit, and&lt;br&gt;
then checks all three facts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postRequests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;websocketUpgrades&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;websocket&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other tests cover alternate loopback forms, private and translated IPv6 ranges,&lt;br&gt;
DNS fan-out, standard public ports, output-directory escape through a junction,&lt;br&gt;
and removal of credentials, query strings, and fragments from report URLs.&lt;/p&gt;

&lt;p&gt;The current release has 21 passing tests. Its self-initiated example audit ran&lt;br&gt;
two PARSAI public routes at desktop and mobile sizes and passed all four cases.&lt;br&gt;
That is product verification, not a client result or a claim that the audited&lt;br&gt;
site has no defects.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the release decision actually checks
&lt;/h2&gt;

&lt;p&gt;For every route × viewport case, the gate records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;navigation status and final origin;&lt;/li&gt;
&lt;li&gt;one visible &lt;code&gt;h1&lt;/code&gt; and at least one visible &lt;code&gt;main&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;completed images with valid natural dimensions;&lt;/li&gt;
&lt;li&gt;horizontal overflow beyond one pixel;&lt;/li&gt;
&lt;li&gt;same-origin console errors, page errors, and failed requests;&lt;/li&gt;
&lt;li&gt;private or reserved network attempts;&lt;/li&gt;
&lt;li&gt;whether the authenticated pinned proxy was observed; and&lt;/li&gt;
&lt;li&gt;any prevented mutating request or WebSocket attempt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It writes machine-readable JSON and a short Markdown report. Exit code &lt;code&gt;0&lt;/code&gt; means&lt;br&gt;
all cases passed, &lt;code&gt;1&lt;/code&gt; means the audit ran and a case failed, and &lt;code&gt;2&lt;/code&gt; means setup&lt;br&gt;
or execution failed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;You need Node.js 22, 24, or 26 on a Playwright-supported operating system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ParsaiWorks/parsai-release-gate.git
&lt;span class="nb"&gt;cd &lt;/span&gt;parsai-release-gate
npm ci
npm run &lt;span class="nb"&gt;install&lt;/span&gt;:browser
npm run audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit the data-only JSON config to use your own public origin and one or two&lt;br&gt;
routes. Private hosts remain off unless you pass the explicit local-testing&lt;br&gt;
flag from the command line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deliberate limits
&lt;/h2&gt;

&lt;p&gt;This is not WCAG conformance testing, penetration testing, load testing,&lt;br&gt;
exhaustive browser coverage, authenticated-flow testing, or proof that no defect&lt;br&gt;
exists. It does not submit forms, execute payments, or modify the target.&lt;/p&gt;

&lt;p&gt;That boundary is part of the product. A small release gate is more useful when&lt;br&gt;
its claims are as constrained as its inputs.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Commercial note:&lt;/strong&gt; the repository is free and MIT-licensed. I also offer a&lt;br&gt;
&lt;a href="https://parsaiapp.ir/en/services/production-web-qa-audit?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=release_gate_v0_1_0" rel="noopener noreferrer"&gt;fixed-scope Production Web QA Audit&lt;/a&gt;&lt;br&gt;
for up to eight public routes, two viewports, agreed checks, evidence screenshots,&lt;br&gt;
and a concise report. It is US$300 with delivery in two business days after&lt;br&gt;
complete inputs and written scope confirmation; please send the inputs first so&lt;br&gt;
I can confirm fit before ordering. One bounded correction for a mismatch with&lt;br&gt;
the written acceptance checklist is included for seven calendar days;&lt;br&gt;
authenticated or private flows, source-code fixes, full accessibility or&lt;br&gt;
security audits, and load testing are excluded.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>testing</category>
      <category>playwright</category>
      <category>security</category>
    </item>
  </channel>
</rss>
