<?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: Jeff W</title>
    <description>The latest articles on DEV Community by Jeff W (@whitmojm).</description>
    <link>https://dev.to/whitmojm</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%2F4100378%2Ffc0ad977-95d6-45fc-bd4e-943f72925db6.png</url>
      <title>DEV Community: Jeff W</title>
      <link>https://dev.to/whitmojm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/whitmojm"/>
    <language>en</language>
    <item>
      <title>Your SSL Cert Works in Chrome. Here's Why It's Broken Everywhere Else.</title>
      <dc:creator>Jeff W</dc:creator>
      <pubDate>Mon, 07 Sep 2026 16:24:10 +0000</pubDate>
      <link>https://dev.to/whitmojm/your-ssl-cert-works-in-chrome-heres-why-its-broken-everywhere-else-3n72</link>
      <guid>https://dev.to/whitmojm/your-ssl-cert-works-in-chrome-heres-why-its-broken-everywhere-else-3n72</guid>
      <description>&lt;p&gt;"The site loads fine in my browser" is one of the least reassuring sentences in TLS debugging. Chrome (and every major browser) does a lot of quiet, generous error-correction that curl, older mobile OSes, most programming-language HTTP clients, and plenty of corporate proxies simply don't. A cert that looks perfectly fine with the padlock icon can still be actively broken for a meaningful slice of real traffic.&lt;/p&gt;

&lt;p&gt;Here's what's actually going on, and how to check for it yourself instead of trusting the padlock.&lt;/p&gt;

&lt;h2&gt;
  
  
  The chain isn't just "your certificate"
&lt;/h2&gt;

&lt;p&gt;A TLS handshake sends your &lt;strong&gt;leaf certificate&lt;/strong&gt; (the one for your actual domain) plus zero or more &lt;strong&gt;intermediate certificates&lt;/strong&gt; that chain it up to a root CA your client already trusts. Browsers ship with a huge cache of known intermediates and will silently fetch a missing one via the AIA (Authority Information Access) extension if your server doesn't send it. Most other clients won't. No caching, no AIA chasing; if your server doesn't hand over the full chain, the handshake just fails.&lt;/p&gt;

&lt;p&gt;This is, by a wide margin, the single most common "works for me" TLS bug.&lt;/p&gt;

&lt;p&gt;Check what your server is actually sending, not what your browser reconstructed for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; example.com:443 &lt;span class="nt"&gt;-servername&lt;/span&gt; example.com &amp;lt; /dev/null 2&amp;gt;/dev/null | openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-issuer&lt;/span&gt; &lt;span class="nt"&gt;-subject&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better, see the whole chain your server sent (count the &lt;code&gt;s:&lt;/code&gt;/&lt;code&gt;i:&lt;/code&gt; pairs — if there's only one certificate and it's not directly signed by a well-known root, you're missing intermediates):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; example.com:443 &lt;span class="nt"&gt;-servername&lt;/span&gt; example.com &lt;span class="nt"&gt;-showcerts&lt;/span&gt; &amp;lt; /dev/null 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Order matters too
&lt;/h2&gt;

&lt;p&gt;Even when the intermediate is present, some servers send it in the wrong order (root before intermediate, or leaf last). The spec technically wants leaf first, then each subsequent issuer in order — plenty of servers get away with the wrong order because, again, browsers correct for it. &lt;code&gt;openssl s_client&lt;/code&gt; will show you the order as sent; a client that validates strictly (which is more common outside browsers than you'd think) will reject an out-of-order chain even though every certificate in it is individually valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  OCSP stapling: not required, but its absence has a cost
&lt;/h2&gt;

&lt;p&gt;Without OCSP stapling, the &lt;em&gt;client&lt;/em&gt; has to go ask the CA "is this cert still valid?" on its own, out-of-band, during the handshake. That's an extra network round-trip to a third party your client may or may not be able to reach quickly (or at all, behind certain corporate proxies/firewalls) — and some clients fail closed on a slow/failed OCSP check rather than just proceeding. Stapling means your server fetches that proof ahead of time and hands it over as part of the handshake itself — one less external dependency in the critical path of every single connection.&lt;/p&gt;

&lt;p&gt;Check whether it's actually working (not just enabled in config):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; example.com:443 &lt;span class="nt"&gt;-servername&lt;/span&gt; example.com &lt;span class="nt"&gt;-status&lt;/span&gt; &amp;lt; /dev/null 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A1&lt;/span&gt; &lt;span class="s2"&gt;"OCSP Response Status"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;successful (0x0)&lt;/code&gt; means it's really stapling a response. Nothing there at all is the far more common finding than you'd expect, even on sites that believe they've turned it on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cipher/protocol mismatches quietly cut off old clients
&lt;/h2&gt;

&lt;p&gt;Disabling TLS 1.0/1.1 and weak ciphers is correct and you should do it, but "correct" and "zero impact" aren't the same thing. Older Android versions, some embedded/IoT HTTP clients, and legacy internal tooling can be stuck on a cipher/protocol combination your server no longer offers. This fails &lt;em&gt;before&lt;/em&gt; certificate validation even happens, so it's easy to misdiagnose as a cert problem when it's actually a negotiation problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; example.com:443 &lt;span class="nt"&gt;-tls1_2&lt;/span&gt; &lt;span class="nt"&gt;-cipher&lt;/span&gt; &lt;span class="s1"&gt;'ECDHE-RSA-AES128-GCM-SHA256'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a specific old client is failing, negotiating with the closest matching protocol/cipher combination from your own machine tells you whether it's even in your server's supported set before you go looking anywhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  SNI: the handshake needs to know who it's talking to
&lt;/h2&gt;

&lt;p&gt;If your server hosts multiple certificates behind one IP (extremely common now), it picks which one to present based on the SNI (Server Name Indication) value the client sends in its &lt;code&gt;ClientHello&lt;/code&gt;. A client that doesn't send SNI at all — some older clients, certain bare-metal health checkers, a &lt;code&gt;curl&lt;/code&gt; command that omits &lt;code&gt;-servername&lt;/code&gt; when testing a non-default port — can get served the wrong certificate, or the server's default/fallback cert, and then fail validation for a hostname that's actually configured perfectly fine. Notice the &lt;code&gt;-servername&lt;/code&gt; flag in every command above — that's not decorative, it's what makes the test represent a real browser request instead of a different, SNI-less one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checking all of this without memorizing openssl flags
&lt;/h2&gt;

&lt;p&gt;The commands above are the ground truth, but running five of them by hand every time you touch a cert is exactly the kind of thing that gets skipped under time pressure — right up until a customer reports "it doesn't work" from some client you didn't test.&lt;/p&gt;

&lt;p&gt;I built a free tool that runs the chain-completeness, order, OCSP-stapling, and TLS protocol/cipher checks above against any domain and shows you the results in one pass:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.shieldingress.com/ssl-checker.html" rel="noopener noreferrer"&gt;SSL Certificate Checker&lt;/a&gt;&lt;/strong&gt; — full chain, key type/size, fingerprints, protocol support matrix, and OCSP status, for any public domain, no account needed.&lt;/p&gt;

&lt;p&gt;(Disclosure: I built this — it's a free tool from &lt;a href="https://www.shieldingress.com" rel="noopener noreferrer"&gt;ShieldIngress&lt;/a&gt;, a WAF/edge security product I run. No catch, no signup — I'd just rather people catch this before a client does.)&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Beyond IP Blocking: Filtering Requests by Header, Query Param, and Cookie</title>
      <dc:creator>Jeff W</dc:creator>
      <pubDate>Wed, 02 Sep 2026 09:24:00 +0000</pubDate>
      <link>https://dev.to/whitmojm/beyond-ip-blocking-filtering-requests-by-header-query-param-and-cookie-55n5</link>
      <guid>https://dev.to/whitmojm/beyond-ip-blocking-filtering-requests-by-header-query-param-and-cookie-55n5</guid>
      <description>&lt;p&gt;Most "block bad traffic" advice starts and ends with IP blocklists. That's a start, but IP-based rules break down fast in practice: attackers rotate through residential proxies, legitimate users share IPs behind NAT/CGNAT, and a single blocked IP can be one bad actor or a whole office building.&lt;/p&gt;

&lt;p&gt;A layer that gets skipped way more often than it should: &lt;strong&gt;matching on the request itself&lt;/strong&gt; — a header, a query parameter, or a cookie — instead of (or alongside) the source IP. Here's when each actually earns its place, with working examples for nginx, Apache, and Express.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Gate an internal/staging endpoint with a shared-secret header
&lt;/h2&gt;

&lt;p&gt;If you have an endpoint that should only ever be hit by your own tooling — a webhook receiver, an internal admin API, a staging environment — don't rely on obscurity or IP allowlisting alone (IPs change, CI runners are ephemeral). Require a header instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# nginx&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/internal/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$http_x_internal_token&lt;/span&gt; &lt;span class="s"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"your-secret-value")&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;403&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="c"&gt;# Apache (mod_rewrite)&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;Location&lt;/span&gt;&lt;span class="sr"&gt; /internal/&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="nc"&gt;RewriteEngine&lt;/span&gt; &lt;span class="ss"&gt;On&lt;/span&gt;
    &lt;span class="nc"&gt;RewriteCond&lt;/span&gt; %{HTTP:X-Internal-Token} !^your-secret-value$
    &lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^ - [F]
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Express middleware&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/internal&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-internal-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INTERNAL_TOKEN&lt;/span&gt;&lt;span class="p"&gt;)&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&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="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Forbidden&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;next&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 is cheap, doesn't care what IP the request comes from, and rotates trivially if it ever leaks (just change the env var).&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Block obvious bot/scraper traffic by User-Agent or missing headers
&lt;/h2&gt;

&lt;p&gt;A shocking amount of low-effort scraping traffic doesn't bother sending a realistic &lt;code&gt;User-Agent&lt;/code&gt;, or omits &lt;code&gt;Accept-Language&lt;/code&gt; entirely — headers a real browser always sends. You can catch a meaningful chunk of junk traffic just by requiring these to be present, before it ever reaches your app logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# nginx&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$http_user_agent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"")&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;403&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="c"&gt;# Apache (mod_rewrite)&lt;/span&gt;
&lt;span class="nc"&gt;RewriteCond&lt;/span&gt; %{HTTP_USER_AGENT} ^-?$
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^ - [F]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This won't stop a sophisticated attacker (they'll just fake a normal browser UA), but it's nearly free and kills a surprising amount of naive automated traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Feature-flag or A/B test at the edge with query params or cookies
&lt;/h2&gt;

&lt;p&gt;Not security-specific, but the same matching mechanism: routing based on a query param (&lt;code&gt;?preview=true&lt;/code&gt;) or a cookie your app sets is how a lot of preview/canary deployments work without needing a separate hostname.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# nginx&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$arg_preview&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"true")&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://staging-backend&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="c"&gt;# Apache (mod_rewrite + mod_proxy)&lt;/span&gt;
&lt;span class="nc"&gt;RewriteCond&lt;/span&gt; %{QUERY_STRING} preview=true
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^(.*)$ http://staging-backend%{REQUEST_URI} [P,L]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The catch
&lt;/h2&gt;

&lt;p&gt;None of this is exotic — the trade-off is that hand-maintained &lt;code&gt;RewriteCond&lt;/code&gt;/&lt;code&gt;if&lt;/code&gt; blocks scattered across config files get messy fast, and a typo in a security-relevant rule is exactly the kind of thing that silently fails open instead of loud. Worth testing each rule against both a request that should pass and one that shouldn't, not just the happy path.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(I work on a managed WAF, &lt;a href="https://www.shieldingress.com" rel="noopener noreferrer"&gt;ShieldIngress&lt;/a&gt; — this is one of the things it does automatically, but everything above works with whatever you're already running.)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>apache</category>
      <category>nginx</category>
    </item>
    <item>
      <title>A Practical Guide to ModSecurity/Coraza SecRule Syntax (and the mistakes that bite)</title>
      <dc:creator>Jeff W</dc:creator>
      <pubDate>Sun, 30 Aug 2026 19:40:10 +0000</pubDate>
      <link>https://dev.to/whitmojm/a-practical-guide-to-modsecuritycoraza-secrule-syntax-and-the-mistakes-that-bite-m00</link>
      <guid>https://dev.to/whitmojm/a-practical-guide-to-modsecuritycoraza-secrule-syntax-and-the-mistakes-that-bite-m00</guid>
      <description>&lt;p&gt;If you run a WAF backed by ModSecurity or Coraza (the Go rewrite most modern setups use now — nginx, HAProxy via SPOE, Traefik plugins, etc.), sooner or later you write a custom &lt;code&gt;SecRule&lt;/code&gt; by hand. The syntax is powerful and genuinely well-designed once it clicks, but the learning curve is real — most people's first custom rule either silently does nothing or blocks traffic they didn't mean to.&lt;/p&gt;

&lt;p&gt;Here's the anatomy of a rule, and the specific mistakes that actually trip people up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of a SecRule
&lt;/h2&gt;

&lt;p&gt;Every rule is three parts: &lt;strong&gt;what to look at&lt;/strong&gt;, &lt;strong&gt;how to match it&lt;/strong&gt;, &lt;strong&gt;what to do&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;SecRule VARIABLE "OPERATOR value" "ACTIONS"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A real one — block any request whose URI contains &lt;code&gt;/wp-admin&lt;/code&gt; from an IP not in your office range:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;SecRule &lt;span class="ss"&gt;REQUEST_URI&lt;/span&gt; "@contains /wp-admin" "id:1000001,phase:2,deny,status:403,msg:'Blocked wp-admin probe'"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;REQUEST_URI&lt;/code&gt; — the variable being inspected. Others you'll reach for constantly: &lt;code&gt;ARGS&lt;/code&gt; (any query/body parameter), &lt;code&gt;REQUEST_HEADERS&lt;/code&gt;, &lt;code&gt;REQUEST_COOKIES&lt;/code&gt;, &lt;code&gt;REQUEST_METHOD&lt;/code&gt;, &lt;code&gt;REMOTE_ADDR&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@contains&lt;/code&gt; — the operator. &lt;code&gt;@streq&lt;/code&gt; (exact match), &lt;code&gt;@beginsWith&lt;/code&gt;/&lt;code&gt;@endsWith&lt;/code&gt;, &lt;code&gt;@rx&lt;/code&gt; (regex), &lt;code&gt;@ipMatch&lt;/code&gt; (IP/CIDR), and the built-in &lt;code&gt;@detectSQLi&lt;/code&gt;/&lt;code&gt;@detectXSS&lt;/code&gt; heuristics are the ones you'll use most.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt;, &lt;code&gt;phase&lt;/code&gt;, &lt;code&gt;deny&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;msg&lt;/code&gt; — actions. Every rule needs a unique &lt;code&gt;id&lt;/code&gt;. &lt;code&gt;phase:2&lt;/code&gt; means "after the request body is parsed" — almost always what you want for anything inspecting &lt;code&gt;ARGS&lt;/code&gt; or &lt;code&gt;REQUEST_BODY&lt;/code&gt;; &lt;code&gt;phase:1&lt;/code&gt; runs before that, headers/URI only.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Combining conditions
&lt;/h2&gt;

&lt;p&gt;Multiple &lt;code&gt;SecRule&lt;/code&gt; lines chained with &lt;code&gt;chain&lt;/code&gt; act as AND — all of them have to match. The gotcha: &lt;strong&gt;only the first line carries the real actions&lt;/strong&gt; (&lt;code&gt;id&lt;/code&gt;, &lt;code&gt;phase&lt;/code&gt;, &lt;code&gt;deny&lt;/code&gt;, etc.); every line after it just needs &lt;code&gt;chain&lt;/code&gt; (except the last, which needs nothing extra):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;SecRule REQUEST_METHOD "@streq POST" "id:1000002,phase:2,deny,status:403,chain"
    SecRule &lt;span class="ss"&gt;REQUEST_URI&lt;/span&gt; "@contains /api/admin" "t:none"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Forget the &lt;code&gt;chain&lt;/code&gt; on a non-final line and you've written two independent rules instead of one AND condition — this is the single most common way a "working" rule turns out to match far more (or far less) than intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Allow-listing instead of blocking
&lt;/h2&gt;

&lt;p&gt;The other half of writing WAF rules is un-blocking something the ruleset got wrong — a legitimate rich-text field that trips an XSS heuristic, for example. Don't just disable the whole rule globally; scope the exemption as narrowly as you can:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;SecRule &lt;span class="ss"&gt;REQUEST_URI&lt;/span&gt; "@beginsWith /editor/save" "id:1000003,phase:2,pass,nolog,ctl:ruleRemoveTargetById=941320;ARGS:content"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ctl:ruleRemoveTargetById=&amp;lt;id&amp;gt;;&amp;lt;VARIABLE&amp;gt;&lt;/code&gt; removes just one field from one rule's inspection &lt;br&gt;
— rule &lt;code&gt;941320&lt;/code&gt; keeps checking everything else on this request, it just stops looking at &lt;code&gt;ARGS:content&lt;/code&gt;. Use &lt;code&gt;ctl:ruleRemoveById=&amp;lt;id&amp;gt;&lt;/code&gt; (no target) only when you're sure the whole rule should be off for requests matching your condition — that's a much bigger exemption than most false-positive fixes actually need.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake almost everyone makes once: rule ID collisions
&lt;/h2&gt;

&lt;p&gt;If you're running the OWASP Core Rule Set (CRS) — most Coraza/ModSecurity setups are — its rules live in &lt;strong&gt;id range 900000–999999&lt;/strong&gt; (and CRS reserves 800000–999999 broadly for its own use). Pick a custom rule ID in that range and you'll get silent, confusing collisions the first time CRS ships an update that happens to use the same number.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;1000000&lt;/code&gt; and up for anything custom. This is genuinely the most common mistake I see — it works fine until the day it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case sensitivity and transformations
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;t:lowercase&lt;/code&gt; before a &lt;code&gt;@streq&lt;/code&gt;/&lt;code&gt;@contains&lt;/code&gt; match makes it case-insensitive; the default (&lt;code&gt;t:none&lt;/code&gt;) is case-sensitive. Easy to forget, and the failure mode is quiet — the rule just doesn't fire on &lt;code&gt;/WP-ADMIN&lt;/code&gt; instead of erroring, which is exactly the kind of gap that gets found in production instead of testing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building these without the syntax fights
&lt;/h2&gt;

&lt;p&gt;Once you know the shape, hand-writing simple rules is fine — but the escaping rules (regex needs different quote-escaping than a literal string match), the chain/action ordering, and the CRS reserved-ID range are exactly the kind of thing that's easy to get subtly wrong under time pressure, mid-incident, at 2am.&lt;/p&gt;

&lt;p&gt;I built a small free tool that generates a correct rule from plain fields — pick the variable, operator, and value, and it handles the syntax, the chaining, the reserved-ID warning, and the block-vs-exempt distinction above for you:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.shieldingress.com/waf-rule-builder.html" rel="noopener noreferrer"&gt;WAF Rule Builder&lt;/a&gt;&lt;/strong&gt; — runs entirely in your browser, nothing you type is sent anywhere, just copy the generated rule into your own config.&lt;/p&gt;

&lt;p&gt;(Disclosure: I built this — it's a free tool from &lt;a href="https://www.shieldingress.com" rel="noopener noreferrer"&gt;ShieldIngress&lt;/a&gt;, a WAF/edge security product I run. No catch, no signup — I'd just rather people not lose an evening to a missing &lt;code&gt;chain&lt;/code&gt; keyword.)&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>6 HTTP Security Headers Your Site Is Probably Missing (and how to fix each one)</title>
      <dc:creator>Jeff W</dc:creator>
      <pubDate>Sat, 29 Aug 2026 15:10:25 +0000</pubDate>
      <link>https://dev.to/whitmojm/6-http-security-headers-your-site-is-probably-missing-and-how-to-fix-each-one-52io</link>
      <guid>https://dev.to/whitmojm/6-http-security-headers-your-site-is-probably-missing-and-how-to-fix-each-one-52io</guid>
      <description>&lt;p&gt;Run any random production site through a headers check and odds are it's missing at least half of the headers below. Not because anyone decided against them — usually because nobody ever added them in the first place. They cost nothing, take one line to set, and most frameworks don't set them for you by default.&lt;/p&gt;

&lt;p&gt;Here's what each one actually does, and the one-liner to add it in a few common stacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Strict-Transport-Security (HSTS)
&lt;/h2&gt;

&lt;p&gt;Tells the browser "never connect to this site over plain HTTP again, even if someone types &lt;code&gt;http://&lt;/code&gt; or links to it that way." Without it, every visit is one stray link or bookmark away from a downgrade-to-HTTP attack.&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
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Express (helmet):&lt;/strong&gt;&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;helmet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hsts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;63072000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;includeSubDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;nginx:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Strict-Transport-Security&lt;/span&gt; &lt;span class="s"&gt;"max-age=63072000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;includeSubDomains"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Content-Security-Policy (CSP)
&lt;/h2&gt;

&lt;p&gt;The single biggest lever against XSS. It tells the browser exactly which origins are allowed to supply scripts, styles, images, etc. — so even if an attacker manages to inject a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag somewhere, the browser refuses to run it if it's not from an allowed source.&lt;/p&gt;

&lt;p&gt;It's also the one people avoid because a bad policy breaks your own site. Start narrow and loosen it as you find real violations (report-only mode is your friend here):&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;Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. X-Frame-Options
&lt;/h2&gt;

&lt;p&gt;Stops your pages from being loaded inside a hidden &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; on someone else's site — the setup behind clickjacking ("click here to win a prize" that's secretly your bank's transfer-confirmation button underneath).&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;X-Frame-Options: DENY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(CSP's &lt;code&gt;frame-ancestors&lt;/code&gt; directive supersedes this in modern browsers, but set both — older clients don't understand &lt;code&gt;frame-ancestors&lt;/code&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  4. X-Content-Type-Options
&lt;/h2&gt;

&lt;p&gt;One value, always the same:&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;X-Content-Type-Options: nosniff
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without it, some browsers will "sniff" a response's actual content and decide to treat it as something other than what the &lt;code&gt;Content-Type&lt;/code&gt; header says — the classic version being an uploaded file that's technically an image but gets executed as script.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Referrer-Policy
&lt;/h2&gt;

&lt;p&gt;Controls how much of your own URL — including query strings, which sometimes contain tokens or IDs you didn't mean to share — gets sent to whatever site a visitor clicks away to.&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;Referrer-Policy: strict-origin-when-cross-origin
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reasonable default: full URL on same-origin navigation, just the origin (no path/query) on cross-origin ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Permissions-Policy
&lt;/h2&gt;

&lt;p&gt;Explicitly turns off browser features your site never uses — camera, microphone, geolocation, USB, etc. Even if you never call these APIs yourself, a compromised third-party script embedded on your page could try to.&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;Permissions-Policy: camera=(), microphone=(), geolocation=()
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bonus: stop announcing your stack
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Server: nginx/1.18.0&lt;/code&gt; and &lt;code&gt;X-Powered-By: Express&lt;/code&gt; don't do anything for your visitors — they just hand an attacker a head start on which known vulnerabilities to try first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server_tokens&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-powered-by&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Express&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Checking your own site
&lt;/h2&gt;

&lt;p&gt;Reading headers off a raw response is annoying to do by hand every time — &lt;code&gt;curl -I&lt;/code&gt; and then squinting for six specific header names. I built a small free tool that just does the check and tells you what's missing and why each one matters:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.shieldingress.com/security-headers-checker.html" rel="noopener noreferrer"&gt;Security Headers Checker&lt;/a&gt;&lt;/strong&gt; — no signup, doesn't store anything you enter, checks all six headers above plus flags the &lt;code&gt;Server&lt;/code&gt;/&lt;code&gt;X-Powered-By&lt;/code&gt; leaks.&lt;/p&gt;

&lt;p&gt;(Disclosure: I built this — it's a free tool from &lt;a href="https://www.shieldingress.com" rel="noopener noreferrer"&gt;ShieldIngress&lt;/a&gt;, a WAF/edge security product I run. The checker itself has no paywall or catch, I'd genuinely rather more sites just have these headers set.)&lt;/p&gt;

&lt;p&gt;If you run it against your own site and find gaps, the snippets above should cover fixing most of them in a few minutes.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>node</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
