<?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: Mayank Jain</title>
    <description>The latest articles on DEV Community by Mayank Jain (@mayank_jain_a705d6aacfd58).</description>
    <link>https://dev.to/mayank_jain_a705d6aacfd58</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%2F2194698%2F664961e9-9e86-4cf0-89ed-eb693e31c3cb.png</url>
      <title>DEV Community: Mayank Jain</title>
      <link>https://dev.to/mayank_jain_a705d6aacfd58</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mayank_jain_a705d6aacfd58"/>
    <language>en</language>
    <item>
      <title>Before you connect that MCP server, run these three commands</title>
      <dc:creator>Mayank Jain</dc:creator>
      <pubDate>Mon, 07 Sep 2026 16:50:31 +0000</pubDate>
      <link>https://dev.to/mayank_jain_a705d6aacfd58/before-you-connect-that-mcp-server-run-these-three-commands-e04</link>
      <guid>https://dev.to/mayank_jain_a705d6aacfd58/before-you-connect-that-mcp-server-run-these-three-commands-e04</guid>
      <description>&lt;p&gt;MCP servers are appearing faster than anyone can vet them. Your editor offers connectors. Claude and ChatGPT have directories. Someone on your team will wire one into a work account this week, and the flow is designed to take about four seconds: click &lt;strong&gt;Connect&lt;/strong&gt;, approve, done.&lt;/p&gt;

&lt;p&gt;Here's what those four seconds actually decide — and three commands you can run first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you're granting, stated plainly
&lt;/h2&gt;

&lt;p&gt;When you connect an MCP server, you are letting a language model call functions on your behalf, in your account, with your permissions, based on its interpretation of sentences typed in a chat box.&lt;/p&gt;

&lt;p&gt;That's not an argument against doing it. It's an argument for knowing three things before you do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What can it do?&lt;/strong&gt; Not "what will it do" — models are probabilistic and prompts get injected. What is the &lt;em&gt;worst&lt;/em&gt; thing the exposed tools make possible?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How is it authenticating?&lt;/strong&gt; A pasted API key that never expires is a very different risk from a one-hour OAuth token you can revoke.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What happens when you disconnect?&lt;/strong&gt; If a key still works afterwards, you didn't disconnect anything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most connector UIs answer none of these. But a well-built MCP server publishes enough to answer #2 yourself, before you click anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 1: what is this server asking for?
&lt;/h2&gt;

&lt;p&gt;Servers that implement the spec properly expose OAuth metadata at a well-known path, and it's readable without any credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/.well-known/oauth-protected-resource/mcp | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real output from a server that does this correctly:&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;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://webcuris.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization_servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://webcuris.com"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scopes_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bearer_methods_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"header"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource_documentation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://webcuris.com/docs/mcp"&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;What you're reading for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;authorization_servers&lt;/code&gt;&lt;/strong&gt; — where you'll actually be typing your password. If it isn't the vendor's own domain, stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;scopes_supported&lt;/code&gt;&lt;/strong&gt; — a short, specific list is a good sign. A scope like &lt;code&gt;full_access&lt;/code&gt; tells you the model has no scoping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;resource_documentation&lt;/code&gt;&lt;/strong&gt; — a server that links its own docs has usually thought about being audited.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Nothing at that path?&lt;/strong&gt; Not automatically damning, but it means the server isn't following the discovery spec, and you're now trusting a UI's summary instead of the server's own declaration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 2: does it actually require authentication?
&lt;/h2&gt;

&lt;p&gt;This one is quick and occasionally alarming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://example.com/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"tools/list"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;You want &lt;code&gt;401&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If an unauthenticated &lt;code&gt;tools/list&lt;/code&gt; returns the tool list, the server is happy to talk to strangers. That may be intentional for a public read-only server — but if it's a server that touches &lt;em&gt;your&lt;/em&gt; data, it means authentication was bolted on somewhere other than the front door, and you should want to know where.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 3: does it point you at the right place?
&lt;/h2&gt;

&lt;p&gt;A correct 401 doesn't just refuse — it tells the client where to authenticate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sD&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://example.com/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"tools/list"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; www-authenticate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;www-authenticate: Bearer realm="Webcuris MCP",
  resource_metadata="https://webcuris.com/.well-known/oauth-protected-resource/mcp"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That header is how a client discovers where to get a token without you pasting anything. Its presence means the server implements the flow rather than improvising it — and improvised auth is where the interesting bugs live.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing to read on the consent screen
&lt;/h2&gt;

&lt;p&gt;When the approval page appears, most people read the app name and click. Read one more thing: &lt;strong&gt;the redirect address your browser will be sent back to.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the field that decides where your authorization code lands. A consent screen that doesn't show it is asking you to approve something you can't see, and a mismatched one is the classic way codes get stolen.&lt;/p&gt;

&lt;h2&gt;
  
  
  After you connect: ask it what it can do
&lt;/h2&gt;

&lt;p&gt;Once connected, before you use it for anything real, ask the assistant directly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"List every tool you have available from this connector, and what each one does."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then read the list as an attacker would. For each tool, finish this sentence: &lt;em&gt;"the worst plausible outcome of the model calling this at the wrong moment is…"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If any answer is worse than "a wasted API call", you've found the thing to think about. Tools that create, delete, send, pay, or invite deserve more scrutiny than tools that read.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're building one
&lt;/h2&gt;

&lt;p&gt;Same list, from the other side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Publish the metadata.&lt;/strong&gt; Let people audit you before they trust you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return 401 with &lt;code&gt;WWW-Authenticate&lt;/code&gt;.&lt;/strong&gt; It's the spec, and it's how clients find your auth without a human pasting secrets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the tool surface small and boring.&lt;/strong&gt; Every administrative endpoint you expose is a sentence away from being called.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call the same code your UI calls.&lt;/strong&gt; A parallel API written for assistants will reimplement 90% of your authorization checks. The missing 10% is the incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make Disconnect mean something.&lt;/strong&gt; Short tokens, rotation, and revocation that takes effect on the next request rather than the next cache expiry.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;The MCP ecosystem is about a year old and the conventions are being set right now by whoever ships first. That's exciting, and it means the usual safety rails — app store review, years of hardening, well-known bad actors — mostly don't exist yet.&lt;/p&gt;

&lt;p&gt;Three curl commands is a low bar. But it's a much higher bar than clicking Connect, which is what most people are doing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I work on &lt;a href="https://webcuris.com" rel="noopener noreferrer"&gt;Webcuris&lt;/a&gt;, which has an MCP server — the real output above is from ours, because it's the one I can show you without picking on somebody. The checks work on any server.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Find your own dangling subdomains before someone else does</title>
      <dc:creator>Mayank Jain</dc:creator>
      <pubDate>Wed, 02 Sep 2026 14:15:18 +0000</pubDate>
      <link>https://dev.to/mayank_jain_a705d6aacfd58/find-your-own-dangling-subdomains-before-someone-else-does-26bj</link>
      <guid>https://dev.to/mayank_jain_a705d6aacfd58/find-your-own-dangling-subdomains-before-someone-else-does-26bj</guid>
      <description>&lt;p&gt;Here's a bug that costs an attacker nothing to find and costs you your domain's reputation.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Someone on your team points a subdomain at a cloud service. One DNS line:
&lt;code&gt;promo.yourcompany.com CNAME yourcompany.pages-host.io&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Months later the cloud account gets deleted. The DNS record does not.&lt;/li&gt;
&lt;li&gt;That &lt;code&gt;CNAME&lt;/code&gt; now points at a name on someone else's platform that belongs to &lt;strong&gt;nobody&lt;/strong&gt; — until a stranger registers it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From that moment they publish whatever they like, on your subdomain, with your certificate and your reputation. It's called subdomain takeover, and the reason it keeps happening is that every step is something a competent team does on a normal day.&lt;/p&gt;

&lt;p&gt;Let's find yours. Everything below runs locally, reads only public data, and takes about a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: enumerate subdomains you've forgotten
&lt;/h2&gt;

&lt;p&gt;You can't audit what you can't list, and the DNS zone in your provider's UI is only the records that still exist — not the historical ones you should be looking for.&lt;/p&gt;

&lt;p&gt;The trick is &lt;strong&gt;certificate transparency logs&lt;/strong&gt;: every TLS certificate ever issued is published to a public, append-only log. If a subdomain ever had HTTPS, it's in there. &lt;a href="https://crt.sh" rel="noopener noreferrer"&gt;crt.sh&lt;/a&gt; makes that queryable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://crt.sh/?q=%25.example.com&amp;amp;output=json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[].name_value'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^\*\.//'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect surprises. This routinely surfaces staging hosts, an old marketing subdomain, and a service someone trialled in 2022.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: find the CNAMEs
&lt;/h2&gt;

&lt;p&gt;Dangling records are almost always &lt;code&gt;CNAME&lt;/code&gt;s — a record pointing at a &lt;em&gt;name&lt;/em&gt; on someone else's infrastructure, rather than an IP you control.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short CNAME promo.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Empty output means it isn't a CNAME, so it isn't this class of bug. Skip it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: check whether the target still exists
&lt;/h2&gt;

&lt;p&gt;This is the actual test. If the CNAME target resolves to nothing, the record is dangling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short A yourcompany.pages-host.io
dig +short CNAME yourcompany.pages-host.io
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Both empty → nobody is answering for that name.&lt;/strong&gt; On a platform that lets anyone register an unused project name, that's a claimable takeover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Find dangling CNAME records on a domain.&lt;/span&gt;
&lt;span class="c"&gt;# Usage: ./dangling.sh example.com&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;:?usage:&lt;span class="p"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt; example.com&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://crt.sh/?q=%25.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;domain&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;output=json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[].name_value'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^\*\.//'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; host&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
      &lt;/span&gt;&lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dig +short CNAME &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/\.$//'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue

      if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dig +short A &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dig +short CNAME &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"DANGLING   &lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;  -&amp;gt;  &lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="k"&gt;else
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ok         &lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;  -&amp;gt;  &lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="k"&gt;fi
    done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Needs &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;jq&lt;/code&gt; and &lt;code&gt;dig&lt;/code&gt;. Output looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ok         www.example.com  -&amp;gt;  example.com
DANGLING   promo.example.com  -&amp;gt;  example.pages-host.io
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything on a &lt;code&gt;DANGLING&lt;/code&gt; line is a live issue. &lt;strong&gt;Delete the DNS record&lt;/strong&gt; — that's the whole fix. If you still need the subdomain, re-create the service first and point the record at something you actually control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this deserves more attention than it gets
&lt;/h2&gt;

&lt;p&gt;Two things make subdomain takeover nastier than its low profile suggests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It defeats the one rule users are taught.&lt;/strong&gt; "Check the URL before you enter your password" fails completely here, because the URL &lt;em&gt;is&lt;/em&gt; real. A phishing page on your own subdomain passes the exact check people are trained to trust — and if any of your cookies are scoped to &lt;code&gt;.yourcompany.com&lt;/code&gt;, they may be readable from it too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody is targeting you.&lt;/strong&gt; Attackers run this exact scan across millions of domains continuously and harvest whatever falls out. Your forgotten subdomain isn't found because someone was hunting you; it's found because someone was hunting everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The habit worth forming
&lt;/h2&gt;

&lt;p&gt;Most security advice is about doing something. This one is about &lt;em&gt;un-doing&lt;/em&gt; — the records, accounts and endpoints that were fine when created and became dangerous only through neglect.&lt;/p&gt;

&lt;p&gt;Run the script. Then put a calendar reminder to run it again next quarter, because the zone you clean today grows a new loose end the next time someone needs a landing page in a hurry.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I work on &lt;a href="https://webcuris.com" rel="noopener noreferrer"&gt;Webcuris&lt;/a&gt;, which maps exposed and dangling subdomains as part of a scan. But the script above is the same check, and it's yours — no account needed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://webcuris.com/blog/the-subdomain-nobody-remembered" rel="noopener noreferrer"&gt;webcuris.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>dns</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What your site tells a stranger before it renders a single pixel</title>
      <dc:creator>Mayank Jain</dc:creator>
      <pubDate>Mon, 31 Aug 2026 14:08:24 +0000</pubDate>
      <link>https://dev.to/mayank_jain_a705d6aacfd58/what-your-site-tells-a-stranger-before-it-renders-a-single-pixel-h7c</link>
      <guid>https://dev.to/mayank_jain_a705d6aacfd58/what-your-site-tells-a-stranger-before-it-renders-a-single-pixel-h7c</guid>
      <description>&lt;p&gt;Type a domain, hit enter, and before anything paints, your server and the browser have already had a short conversation. The server sends back response headers — and to anyone who knows how to read them, those headers are a fairly candid statement about how seriously that site takes its own security.&lt;/p&gt;

&lt;p&gt;Attackers read them first. Not because headers are exciting, but because they're free: no login, no exploit, nothing in your logs. One request tells them whether you're worth more attention.&lt;/p&gt;

&lt;p&gt;Let's read them the way they do. Open devtools → Network → reload → click the first request → Response Headers. Follow along on your own site.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;Strict-Transport-Security&lt;/code&gt; — does your HTTPS actually mean anything?
&lt;/h2&gt;

&lt;p&gt;You have a cert. The padlock shows. But think about what happens when someone types your domain &lt;em&gt;without&lt;/em&gt; the scheme — which is everyone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET http://yourdomain.com     ← plaintext, before your redirect exists
301 → https://yourdomain.com  ← too late for that first hop
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a hostile network, an attacker in the middle can intercept that first request and simply never deliver your redirect, keeping the victim on plaintext they won't notice.&lt;/p&gt;

&lt;p&gt;HSTS fixes it by telling the browser: &lt;em&gt;for the next N seconds, never contact this host over HTTP at all.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Reading it:&lt;/strong&gt; no header → every first visit is interceptable. A &lt;code&gt;max-age&lt;/code&gt; far under a year is a weak commitment. No &lt;code&gt;includeSubDomains&lt;/code&gt; → protection stops at the apex. HTTP Archive's 2025 crawl found HSTS on about 36% of pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;Content-Security-Policy&lt;/code&gt; — what happens when something &lt;em&gt;does&lt;/em&gt; get injected?
&lt;/h2&gt;

&lt;p&gt;The most consequential header, and the least deployed. CSP tells the browser which sources of script are allowed to execute. Anything else — an injected &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; from a comment field, a compromised third-party widget, a supply-chain hit on a dependency — the browser refuses to run.&lt;/p&gt;

&lt;p&gt;Its absence isn't a vulnerability by itself. It's the absence of the safety net under every other mistake you might make. With a solid CSP, an XSS bug is often an annoyance. Without one, the same bug owns the page.&lt;/p&gt;

&lt;p&gt;Same crawl: &lt;strong&gt;only 21.9% of sites send a CSP at all.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;# what you want (nonce-based)
Content-Security-Policy: script-src 'nonce-{random}' 'strict-dynamic'; object-src 'none'; base-uri 'none'

# what mostly gets shipped
Content-Security-Policy: script-src 'self' 'unsafe-inline'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Reading it:&lt;/strong&gt; &lt;code&gt;'unsafe-inline'&lt;/code&gt; in &lt;code&gt;script-src&lt;/code&gt; substantially undoes the protection — it permits exactly the inline execution injection relies on. A nonce or hash policy is the real thing. It's more work, which is why it's rarer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One warning from our own production, since I've now paid for this lesson: if you go nonce-based on Next.js App Router, make sure every interactive page renders dynamically. A statically prerendered page has no request, so it gets no nonce, so the browser blocks &lt;em&gt;every&lt;/em&gt; script on it. Dev never shows this — &lt;code&gt;next dev&lt;/code&gt; renders everything dynamically.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;Set-Cookie&lt;/code&gt; — how carefully is the session handled?
&lt;/h2&gt;

&lt;p&gt;Your session cookie &lt;em&gt;is&lt;/em&gt; the logged-in user. Three attributes decide how hard it is to steal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Set-Cookie: session=…; HttpOnly; Secure; SameSite=Lax; Path=/
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;HttpOnly&lt;/code&gt; — JavaScript can't read it. Without this, any script injection exfiltrates sessions directly.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Secure&lt;/code&gt; — never sent over plaintext.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SameSite&lt;/code&gt; — restricts cross-site sending; the core CSRF defence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reading it:&lt;/strong&gt; a session cookie without &lt;code&gt;HttpOnly&lt;/code&gt; tells an attacker that one XSS gets them &lt;em&gt;accounts&lt;/em&gt;, not just defacement.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;Server&lt;/code&gt; / &lt;code&gt;X-Powered-By&lt;/code&gt; — what exactly are you running?
&lt;/h2&gt;

&lt;p&gt;Plenty of stacks announce their software and version by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Server: Apache/2.4.41
X-Powered-By: PHP/7.4.3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a shortcut: instead of probing, they look up known vulnerabilities for that exact build. Removing these isn't a real defence — a determined attacker fingerprints you anyway — but publishing them narrows their work from hours to seconds, and a version string also announces, at a glance, whether you're years behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meta-signal
&lt;/h2&gt;

&lt;p&gt;Here's the part that matters more than any single header. Someone reading your response isn't only collecting weaknesses — they're forming a judgement about the people who run this site. These headers are cheap; most are one line of config. A site with HSTS, a real CSP and properly flagged cookies says &lt;em&gt;someone here thought about this, and probably thought about the harder things too.&lt;/em&gt; A site with none of them says the opposite, before any deeper probing begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your five-minute checklist
&lt;/h2&gt;

&lt;p&gt;Devtools open, first request selected, four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is &lt;code&gt;Strict-Transport-Security&lt;/code&gt; present, with a long &lt;code&gt;max-age&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Is there a &lt;code&gt;Content-Security-Policy&lt;/code&gt; — and does it avoid &lt;code&gt;unsafe-inline&lt;/code&gt; for scripts?&lt;/li&gt;
&lt;li&gt;Does the session cookie carry &lt;code&gt;HttpOnly&lt;/code&gt;, &lt;code&gt;Secure&lt;/code&gt; and &lt;code&gt;SameSite&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Are you advertising exact software versions you don't need to?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Whatever you find is the same thing a stranger sees on their first request. The only difference is that they've already looked.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I work on &lt;a href="https://webcuris.com" rel="noopener noreferrer"&gt;Webcuris&lt;/a&gt; — these headers are among the first things we read on a scan, along with TLS, email auth and exposed subdomains. But you don't need us for the checklist above; the devtools you already have will answer all four questions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://webcuris.com/blog/what-your-website-tells-a-stranger" rel="noopener noreferrer"&gt;webcuris.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>devops</category>
      <category>beginners</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Mayank Jain</dc:creator>
      <pubDate>Fri, 28 Aug 2026 17:13:18 +0000</pubDate>
      <link>https://dev.to/mayank_jain_a705d6aacfd58/-1ic8</link>
      <guid>https://dev.to/mayank_jain_a705d6aacfd58/-1ic8</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/mayank_jain_a705d6aacfd58/the-csp-that-killed-our-own-form-a-production-only-nextjs-bug-4hil" class="crayons-story__hidden-navigation-link"&gt;The CSP that killed our own form (a production-only Next.js bug)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/mayank_jain_a705d6aacfd58" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2194698%2F664961e9-9e86-4cf0-89ed-eb693e31c3cb.png" alt="mayank_jain_a705d6aacfd58 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/mayank_jain_a705d6aacfd58" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Mayank Jain
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Mayank Jain
                
                
              
              &lt;div id="story-author-preview-content-4503122" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/mayank_jain_a705d6aacfd58" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2194698%2F664961e9-9e86-4cf0-89ed-eb693e31c3cb.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Mayank Jain&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/mayank_jain_a705d6aacfd58/the-csp-that-killed-our-own-form-a-production-only-nextjs-bug-4hil" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Aug 27&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/mayank_jain_a705d6aacfd58/the-csp-that-killed-our-own-form-a-production-only-nextjs-bug-4hil" id="article-link-4503122"&gt;
          The CSP that killed our own form (a production-only Next.js bug)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/security"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;security&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/debugging"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;debugging&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/nextjs"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;nextjs&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/mayank_jain_a705d6aacfd58/the-csp-that-killed-our-own-form-a-production-only-nextjs-bug-4hil" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/mayank_jain_a705d6aacfd58/the-csp-that-killed-our-own-form-a-production-only-nextjs-bug-4hil#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>The CSP that killed our own form (a production-only Next.js bug)</title>
      <dc:creator>Mayank Jain</dc:creator>
      <pubDate>Thu, 27 Aug 2026 09:21:18 +0000</pubDate>
      <link>https://dev.to/mayank_jain_a705d6aacfd58/the-csp-that-killed-our-own-form-a-production-only-nextjs-bug-4hil</link>
      <guid>https://dev.to/mayank_jain_a705d6aacfd58/the-csp-that-killed-our-own-form-a-production-only-nextjs-bug-4hil</guid>
      <description>&lt;p&gt;A user wrote to us with the kind of bug report that makes your stomach drop:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;"The forgot-password form is not working at all."&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Not "it's slow." Not "I got an error." Not working at all.&lt;/p&gt;

&lt;p&gt;So we did what you do. Checked the error tracker: clean. Checked the server logs: clean. Checked the audit log that records every password-reset request: zero requests, ever. The endpoint worked perfectly from curl. The page returned a healthy 200. Every test in a 3,500-test suite was green.&lt;/p&gt;

&lt;p&gt;And the form was completely dead in production.&lt;/p&gt;

&lt;p&gt;Here's the story of why — and why this class of bug is invisible in development, invisible in CI, and invisible to every server-side monitor you own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup: a CSP we were proud of
&lt;/h2&gt;

&lt;p&gt;Our site ships a strict Content-Security-Policy. No unsafe-inline for scripts — instead, every response gets a fresh cryptographic nonce, stamped into the header by middleware:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;csp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildCsp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// script-src 'nonce-...' 'strict-dynamic' ...&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&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="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-nonce&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nonce&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-security-policy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&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="nx"&gt;response&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-security-policy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&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;Next.js plays along beautifully: it reads the nonce out of the script-src directive and stamps it onto every script tag it injects for hydration. Browser sees matching nonces, scripts run, page hydrates. This is the textbook setup — a security-scanner company can hardly ship unsafe-inline.&lt;/p&gt;

&lt;p&gt;There is just one sentence in the fine print, and it bit us:&lt;/p&gt;

&lt;p&gt;Next.js only injects the nonce on pages that are rendered dynamically — per request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: one page quietly went static
&lt;/h2&gt;

&lt;p&gt;In the Next.js App Router, pages that don't read request-time data get statically prerendered at build time. That's normally a gift: free speed. You can see which is which in the build output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├ ○ /forgot-password      ← static  (prerendered at build time)
├ ƒ /login                ← dynamic (rendered per request)
├ ƒ /reset-password       ← dynamic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See it? Our /forgot-password page — a page with a form, a bot-check widget, client-side validation — was the one interactive page in the whole build that went static. Its HTML was baked once at build time. And at build time, there is no request, so there is no nonce.&lt;/p&gt;

&lt;p&gt;Result: the page shipped script tags with no nonce at all, into responses whose CSP header demanded one.&lt;/p&gt;

&lt;p&gt;The browser did exactly what we had asked it to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Refused to execute script '.../_next/static/chunks/main-app.js'
because it violates the following Content Security Policy directive: ...
Refused to execute script '.../_next/static/chunks/app/layout.js' ...
Refused to execute script ... (× every single chunk)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;*&lt;em&gt;Every script on the page: blocked. By our own security header.&lt;br&gt;
*&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why "not working at all" was the perfect description
&lt;/h2&gt;

&lt;p&gt;With zero JavaScript executing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React never hydrated. No component ever mounted.&lt;/li&gt;
&lt;li&gt;The bot-check widget never rendered. It's loaded by script.&lt;/li&gt;
&lt;li&gt;The submit handler never attached. So the button fell back to what a button inside a  does natively: submit-and-reload. Click → page flickers → same page. To a user, that is precisely "not working at all."&lt;/li&gt;
&lt;li&gt;No request ever left the browser. Which is why every server-side signal we had — logs, error tracker, audit trail — was spotless. You cannot log a request that was never made.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the cruelest part:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;next dev&lt;/code&gt; renders everything dynamically. The bug cannot exist in development.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;curl&lt;/code&gt; doesn't execute JavaScript. The page 200s all day.&lt;/li&gt;
&lt;li&gt;Unit tests don't run a browser against the production build with the production CSP.&lt;/li&gt;
&lt;li&gt;The only place this bug existed was the one place we weren't looking: a real browser, pointed at the production build, with the console open.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The two-minute diagnosis (once we looked in the right place)
&lt;/h2&gt;

&lt;p&gt;Opening the production page with DevTools showed a wall of red CSP violations. Then one comparison nailed it — count nonced scripts per page:&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="c1"&gt;// in the console, on each page:&lt;/span&gt;
&lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scripts&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
&lt;span class="c1"&gt;// /login:            15  ✅&lt;/span&gt;
&lt;span class="c1"&gt;// /pricing:          19  ✅&lt;/span&gt;
&lt;span class="c1"&gt;// /forgot-password:   0  ❌  ← there's your dead page&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cross-checked against the build route table: /forgot-password had the ○ (static) marker. Its sibling /reset-password was dynamic — because someone had once added the magic line there and not on the twin page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: one line, plus the part that actually matters
&lt;/h2&gt;

&lt;p&gt;The one line:&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="c1"&gt;// app/forgot-password/page.tsx&lt;/span&gt;

&lt;span class="c1"&gt;// Under a nonce CSP, a statically prerendered page ships un-nonced&lt;/span&gt;
&lt;span class="c1"&gt;// scripts and the browser blocks all of them. This line is load-bearing.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;force-dynamic&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;Rebuild: ○ becomes ƒ, nonces flow, widget renders, form lives.&lt;/p&gt;

&lt;p&gt;But a one-line fix for a silent, production-only, whole-page failure deserves more than one line. Two things we added:&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;1. A regression test that names the failure class, not the instance:&lt;br&gt;
*&lt;/em&gt;&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="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no auth page may be statically prerendered&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;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Under a nonce CSP, a static page is a dead form waiting&lt;/span&gt;
  &lt;span class="c1"&gt;// for a user to find it.&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;signup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forgot-password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reset-password&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;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; forces dynamic rendering`&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`app/(auth)/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/page.tsx`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`export const dynamic = "force-dynamic"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&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;&lt;strong&gt;2. A QA rule we now follow on every release:&lt;/strong&gt; at least one pass through the production deployment, in a real browser, with the console open. A page can return 200, look pixel-perfect, and be completely dead. Your server cannot tell you about requests that never happen — only the browser knows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Your security controls are part of your attack surface — against yourself. A strict CSP is worth it, but every hardening measure needs a test that proves the product still works under it.&lt;/li&gt;
&lt;li&gt;Static vs. dynamic rendering is a security-relevant decision in the App Router, not just a performance one. If your CSP uses nonces, an interactive page that goes static is broken by construction.&lt;/li&gt;
&lt;li&gt;Absence of errors is not evidence of health. Every monitoring signal we had was green while 100% of users hit a dead form. The failure lived client-side, before the first request.&lt;/li&gt;
&lt;li&gt;Read your build output. That little ○/ƒ column is a security audit nobody performs.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;We build Webcuris(&lt;a href="https://webcuris.com/" rel="noopener noreferrer"&gt;https://webcuris.com/&lt;/a&gt;) — continuous security assessment for websites and repos: CSP, TLS, headers, dependencies, and whether last month's fix actually held. This bug shipped in our own product, which is exactly why we believe in re-checking everything, continuously. You can scan one page free, no signup. (&lt;a href="https://webcuris.com/scan" rel="noopener noreferrer"&gt;https://webcuris.com/scan&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>security</category>
      <category>debugging</category>
      <category>nextjs</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
