<?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: Yun</title>
    <description>The latest articles on DEV Community by Yun (@yun_93392ee179711ec1ea4ce).</description>
    <link>https://dev.to/yun_93392ee179711ec1ea4ce</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%2F4200173%2F3e443902-1fc9-4089-9de0-7210532c4813.png</url>
      <title>DEV Community: Yun</title>
      <link>https://dev.to/yun_93392ee179711ec1ea4ce</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yun_93392ee179711ec1ea4ce"/>
    <language>en</language>
    <item>
      <title>Building a Passive PHP2Shell Header Inspector with Next.js and Cloudflare</title>
      <dc:creator>Yun</dc:creator>
      <pubDate>Thu, 22 Oct 2026 09:52:53 +0000</pubDate>
      <link>https://dev.to/yun_93392ee179711ec1ea4ce/building-a-passive-php2shell-header-inspector-with-nextjs-and-cloudflare-26ml</link>
      <guid>https://dev.to/yun_93392ee179711ec1ea4ce/building-a-passive-php2shell-header-inspector-with-nextjs-and-cloudflare-26ml</guid>
      <description>&lt;p&gt;When maintaining a PHP website, a simple question can lead to several different investigations:&lt;/p&gt;

&lt;p&gt;Which PHP version does the website advertise? Which version actually serves requests? Has the hosting provider backported a fix? Are temporary controls deployed correctly?&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://checkphp2shell.com/" rel="noopener noreferrer"&gt;PHP2Shell Emergency Response Hub&lt;/a&gt; to help organize those steps. It combines a passive HTTP header inspector, a manual PHP version evaluator, and configuration guidance for several deployment environments.&lt;/p&gt;

&lt;p&gt;The most important design decision was defining what each result could establish. Public headers provide useful evidence, but they cannot certify that a website is patched.&lt;/p&gt;

&lt;p&gt;The application uses Next.js App Router, TypeScript, Tailwind CSS, and &lt;code&gt;lucide-react&lt;/code&gt;. Here are the implementation choices and limitations behind it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One inspection engine, two runtime adapters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The advisory pages are statically exported to Cloudflare Pages. The interactive inspector calls a Pages Function at &lt;code&gt;/api/check-url&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The project also retains a Next.js API route for Node deployments. Both adapters use the same validation, fingerprinting, and assessment logic.&lt;/p&gt;

&lt;p&gt;The shared engine depends on a small transport interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ResolvedAddress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PassiveTransport&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ResolvedAddress&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HEAD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;addresses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ResolvedAddress&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps runtime-specific networking separate from the interpretation of evidence. The same response headers should produce the same assessment in either deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keeping outbound requests deliberately limited&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The browser submits a JSON POST to our API. Requests from our service to the target website use ordinary HEAD, with GET fallback only when HEAD returns &lt;code&gt;405&lt;/code&gt; or &lt;code&gt;501&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The inspection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accepts public HTTP/HTTPS homepage URLs on default ports.&lt;/li&gt;
&lt;li&gt;Sends no exploit payload, multipart POST body, authentication credentials, or cookies.&lt;/li&gt;
&lt;li&gt;Does not follow redirects.&lt;/li&gt;
&lt;li&gt;Discards response bodies rather than inspecting page content.&lt;/li&gt;
&lt;li&gt;Uses one five-second deadline shared by DNS resolution, HEAD, and any GET fallback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;code&gt;403&lt;/code&gt; does not trigger another request method in an attempt to get around the restriction.&lt;/p&gt;

&lt;p&gt;The request also comes from our service, not the visitor’s authenticated browser. Its response can differ from what the visitor sees because of access controls, CDN behavior, or location.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A URL checker needs an explicit network boundary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Accepting a URL creates an SSRF risk even when the intended request is read-only.&lt;/p&gt;

&lt;p&gt;Input validation rejects IP literals, local and reserved hostnames, credentials, custom ports, paths, query strings, and fragments. DNS validation rejects private or special-use addresses, including mixed public/private answers.&lt;/p&gt;

&lt;p&gt;The transport details matter too.&lt;/p&gt;

&lt;p&gt;In Node, the connection is pinned to a validated address while preserving the original hostname for Host, TLS SNI, and certificate verification.&lt;/p&gt;

&lt;p&gt;On Cloudflare, the transport uses public DNS preflight checks and public Internet fetch behavior. The deployment explicitly enables &lt;code&gt;global_fetch_strictly_public&lt;/code&gt; and avoids origin or private-network bindings in this transport. Cloudflare documents how that flag changes routing for requests to a Worker’s own zone. &lt;a href="https://developers.cloudflare.com/workers/configuration/compatibility-flags/#global-fetch-strictly-public" rel="noopener noreferrer"&gt;Compatibility flag documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;DNS preflight alone is insufficient protection against a second, different resolution during connection establishment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separating observations from assessments&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The inspector looks for PHP version signatures in &lt;code&gt;Server&lt;/code&gt; and &lt;code&gt;X-Powered-By&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It also recognizes weaker application clues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PHPSESSID&lt;/code&gt; and &lt;code&gt;laravel_session&lt;/code&gt; cookie names.&lt;/li&gt;
&lt;li&gt;Recognized WordPress cookie names.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X-Pingback&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;Link&lt;/code&gt; header containing &lt;code&gt;wp-json&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cookie values are never included in the returned report.&lt;/p&gt;

&lt;p&gt;These clues suggest PHP or a PHP-based application. They do not identify the installed binary, its package revision, or every backend worker.&lt;/p&gt;

&lt;p&gt;Version results therefore distinguish several cases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Interpretation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;threshold_match&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The advertised version meets a reviewed upstream fix threshold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;older_header&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The advertised version is older or belongs to an EOL branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vendor_check&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A package suffix requires vendor-specific verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unknown_version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The version is a prerelease or outside the reviewed matrix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;conflicting_versions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Multiple different PHP versions were advertised&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;version_hidden&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PHP-related clues exist without a complete version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no_php_evidence&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;This response contains no recognized PHP clues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;response_limited&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The response cannot establish the origin application’s patch posture&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An older version string may belong to a package with backported security fixes. A newer-looking string may be stale, modified, or unrelated to some of the serving workers.&lt;/p&gt;

&lt;p&gt;The report includes header provenance, observation time, a rule revision, and links to the reviewed PHP advisories so users can inspect the basis for the result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Response context takes priority&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A response advertising a patched-looking version might be a proxy error page.&lt;/p&gt;

&lt;p&gt;I handle response context before presenting the version assessment. The classification rule is small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;responseContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;redirect&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;access_limited&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a non-success response, the main version result becomes &lt;code&gt;response_limited&lt;/code&gt;. The report retains the header-only observation separately, but the interface does not offer configuration prompts based on that restricted response.&lt;/p&gt;

&lt;p&gt;Similarly, an observed Cloudflare, Sucuri, Akamai, or CloudFront signature establishes only that a recognizable product marker appeared. It cannot confirm whether a WAF is enabled, which rules apply, or whether protection is effective.&lt;/p&gt;

&lt;p&gt;The API consistently returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"wafProtection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unverified"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Temporary mitigation and complete remediation remain separate&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This distinction shapes the configuration generator.&lt;/p&gt;

&lt;p&gt;The PHP advisories describe out-of-memory corruption and stale request-owned global state. They identify patched releases for both issues and explain why configuration measures have narrower coverage. &lt;a href="https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w" rel="noopener noreferrer"&gt;OOM corruption advisory&lt;/a&gt;, &lt;a href="https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp" rel="noopener noreferrer"&gt;stale global state advisory&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, administratively disabling session upload progress addresses the demonstrated session lifecycle path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;session.upload_progress.enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That does not cover every affected lifecycle path. The advisory also requires restarting workers for this measure. Applying an INI example is not a universal fix or a guarantee of zero downtime.&lt;/p&gt;

&lt;p&gt;The site provides environment-specific guidance for php.ini, PHP-FPM, Nginx, Apache, Cloudflare WAF, ModSecurity, Docker, and Kubernetes. Users must confirm the applicable deployment and prerequisites before copying configurations.&lt;/p&gt;

&lt;p&gt;Complete remediation requires verifying a fixed runtime or an appropriate vendor security package, deploying it across the affected instances, and restarting or replacing serving workers as required. The actual Web PHP runtime must be checked: CLI &lt;code&gt;php -v&lt;/code&gt; may describe a different installation.&lt;/p&gt;

&lt;p&gt;Patching also does not establish that earlier compromise never occurred. Investigation and recovery remain separate responsibilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preventing a misleading handoff into configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Another failure mode appears after the inspection succeeds.&lt;/p&gt;

&lt;p&gt;Suppose someone configures website A, then inspects website B and selects a mitigation example. Carrying A’s version, deployment, or confirmations into B’s workflow could produce convincing but inappropriate instructions.&lt;/p&gt;

&lt;p&gt;Entering a configuration review now clears the previous environment and verification state. The checked domain is displayed, and the user must confirm its actual Web PHP version and deployment again.&lt;/p&gt;

&lt;p&gt;An advertised header version never silently becomes confirmed runtime input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing behavior and bounding public usage&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The inspector has per-client, per-target, instance, and concurrency limits, alongside Cloudflare edge burst protection. These controls are scoped to their respective runtime instances or edge locations; they are not a strictly synchronized global quota.&lt;/p&gt;

&lt;p&gt;The frontend handles both application JSON errors and HTML &lt;code&gt;429&lt;/code&gt; responses generated at the edge.&lt;/p&gt;

&lt;p&gt;For the release, validation included 28 unit tests and 147 passing desktop/mobile browser tests, with one duplicate mobile API test intentionally skipped. Coverage included SSRF restrictions, fallback methods, shared deadlines, uncertain versions, response limitations, and configuration state resets.&lt;/p&gt;

&lt;p&gt;Production verification covered all five languages—English, Japanese, German, Spanish, and Portuguese. Real passive requests targeted our own website. Older-version configuration scenarios used controlled response fixtures, recorded separately from remote observations.&lt;/p&gt;

&lt;p&gt;You can try the tool at &lt;a href="https://checkphp2shell.com/" rel="noopener noreferrer"&gt;checkphp2shell.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I would especially welcome feedback on the assessment wording: which results remain ambiguous, and what additional evidence would help you decide when to move from an external observation to a server-side investigation?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>php</category>
      <category>security</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
