<?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: Dipta</title>
    <description>The latest articles on DEV Community by Dipta (@dip_032d2fe1959e1990ddbb1).</description>
    <link>https://dev.to/dip_032d2fe1959e1990ddbb1</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3930474%2F303a9bf6-548a-4748-876c-40363b9fd318.jpg</url>
      <title>DEV Community: Dipta</title>
      <link>https://dev.to/dip_032d2fe1959e1990ddbb1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dip_032d2fe1959e1990ddbb1"/>
    <language>en</language>
    <item>
      <title>CSRF, and the cookie flag</title>
      <dc:creator>Dipta</dc:creator>
      <pubDate>Sat, 30 May 2026 12:35:20 +0000</pubDate>
      <link>https://dev.to/dip_032d2fe1959e1990ddbb1/csrf-and-the-cookie-flag-4930</link>
      <guid>https://dev.to/dip_032d2fe1959e1990ddbb1/csrf-and-the-cookie-flag-4930</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"https://bank.com/transfer"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"to"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"attacker"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"amount"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"10000"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;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;forms&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five lines of HTML on a malicious page. When a user who's logged into &lt;code&gt;bank.com&lt;/code&gt; in another tab visits this page, the browser auto-submits the form, attaches their session cookie, and ten thousand dollars leave their account.&lt;/p&gt;

&lt;p&gt;They didn't click anything. The malicious site didn't see their password. There was no XSS, no breach, no leak in the traditional sense. The browser did exactly what it was designed to do.&lt;/p&gt;

&lt;p&gt;That's CSRF — Cross-Site Request Forgery — and it's been the classic "confused deputy" attack on the web for two decades. Let's walk through what makes it work, why CORS doesn't help, and the one cookie flag that mostly killed it around 2020.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the browser attaches your cookie to that request
&lt;/h2&gt;

&lt;p&gt;Cookies belong to a domain. When you log into &lt;code&gt;bank.com&lt;/code&gt;, the bank sets a session cookie in your browser:&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=abc123; HttpOnly
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From that point on, every single request your browser sends to &lt;code&gt;bank.com&lt;/code&gt; carries that cookie. Every page load. Every API call. Every image fetch. The browser does it automatically, without asking, and regardless of who triggered the request.&lt;/p&gt;

&lt;p&gt;That last word is the door CSRF walks through. The browser attaches the cookie based on where the request is &lt;em&gt;going&lt;/em&gt;, not where it came &lt;em&gt;from&lt;/em&gt;. So when &lt;code&gt;evil.com&lt;/code&gt; triggers a &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;bank.com/transfer&lt;/code&gt;, the browser sees a request destined for &lt;code&gt;bank.com&lt;/code&gt;, looks up the cookies for &lt;code&gt;bank.com&lt;/code&gt;, and attaches them. As far as the bank's server can tell, the request looks exactly like one the user submitted from inside the bank's own page.&lt;/p&gt;

&lt;p&gt;This is the "confused deputy" idea. Your browser is the deputy. It has authority on your behalf (your cookies). And it's been tricked into using that authority for someone else's benefit. The server has no way to tell the difference, because from its point of view, there isn't one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CORS doesn't help
&lt;/h2&gt;

&lt;p&gt;It's a fair guess that CORS would stop this. The request is cross-origin. CORS controls cross-origin behavior. So CORS should block it, right?&lt;/p&gt;

&lt;p&gt;It doesn't.&lt;/p&gt;

&lt;p&gt;CORS controls what JavaScript can &lt;em&gt;read&lt;/em&gt; from a cross-origin response. It says nothing about whether the request gets &lt;em&gt;sent&lt;/em&gt;. And in CSRF, the attacker doesn't care about reading anything back. The damage is done by the time the response comes back.&lt;/p&gt;

&lt;p&gt;There's a second wrinkle that makes this worse. Plain form submissions and image loads — both of which can carry cookies — don't trigger a CORS preflight at all. The browser just sends them. So a &lt;code&gt;&amp;lt;form method="POST"&amp;gt;&lt;/code&gt; posting from &lt;code&gt;evil.com&lt;/code&gt; to &lt;code&gt;bank.com&lt;/code&gt; is, from the browser's perspective, an ordinary cross-site form submission. There's nothing for CORS to check, and nothing for the server to refuse.&lt;/p&gt;

&lt;p&gt;CORS protects what JavaScript can &lt;em&gt;see&lt;/em&gt;. CSRF is about what the server is tricked into &lt;em&gt;doing&lt;/em&gt;. Different attacks, different layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually stops it
&lt;/h2&gt;

&lt;p&gt;So what does? Two defenses. One classic, one modern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The CSRF token (classic).&lt;/strong&gt; When &lt;code&gt;bank.com&lt;/code&gt; renders the transfer page, the server embeds a random, unguessable token in the form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/transfer"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"to"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"amount"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"csrf_token"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"9f3e8a7b12..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server checks the token when the form comes back. If it matches the one issued to this session, the request is legitimate. If it doesn't, or it's missing, the request is rejected.&lt;/p&gt;

&lt;p&gt;This works because &lt;code&gt;evil.com&lt;/code&gt; has no way to learn the token. Same-origin policy stops it from reading &lt;code&gt;bank.com&lt;/code&gt;'s pages, which is where the token lives. The attacker can build a fake form, but they can't put a valid token in it. The request arrives at the server with no token, the check fails, and the request is dropped. This is what Django, Rails, and Laravel do by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;SameSite&lt;/code&gt; cookies (modern).&lt;/strong&gt; Set the cookie like this:&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=abc123; HttpOnly; Secure; SameSite=Lax
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SameSite=Lax&lt;/code&gt; tells the browser: don't attach this cookie to requests coming from a different site, except for top-level GET navigations (clicking a link still works). So when &lt;code&gt;evil.com&lt;/code&gt; triggers a &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;bank.com&lt;/code&gt;, the browser sees a cross-site origin and drops the cookie from the request. The bank receives an unauthenticated request and rejects it. The whole attack collapses at the cookie layer, before the server has to think about it.&lt;/p&gt;

&lt;p&gt;Chrome started treating unspecified cookies as &lt;code&gt;SameSite=Lax&lt;/code&gt; by default in 2020. Firefox and Safari followed. A huge chunk of CSRF on the web was quietly killed by that one change. If you're shipping anything new, set &lt;code&gt;SameSite=Lax&lt;/code&gt; (or &lt;code&gt;Strict&lt;/code&gt; for high-sensitivity cookies) explicitly, and you're most of the way there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing these defenses don't help with
&lt;/h2&gt;

&lt;p&gt;Neither of them helps against XSS. If an attacker can run JavaScript inside your own origin — say, a forgotten &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;, an unsanitized rich-text input, a compromised dependency — the cookie is "same site" to that script, and any CSRF token in the page is readable. Both defenses assume your origin is trustworthy. If it isn't, CSRF is the smallest of your problems.&lt;/p&gt;

&lt;p&gt;This is the boring lesson that keeps showing up across web security: each defense covers one layer, and stacking them is what keeps you safe. &lt;code&gt;SameSite&lt;/code&gt; for CSRF. &lt;code&gt;HttpOnly&lt;/code&gt; for cookie theft. CSP for XSS. None of them alone is enough. Together they cover most real cases.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>frontend</category>
      <category>security</category>
    </item>
    <item>
      <title>What's actually going on with CORS, under the hood</title>
      <dc:creator>Dipta</dc:creator>
      <pubDate>Sat, 23 May 2026 16:29:46 +0000</pubDate>
      <link>https://dev.to/dip_032d2fe1959e1990ddbb1/whats-actually-going-on-with-cors-under-the-hood-l10</link>
      <guid>https://dev.to/dip_032d2fe1959e1990ddbb1/whats-actually-going-on-with-cors-under-the-hood-l10</guid>
      <description>&lt;p&gt;CORS is one of those things every web developer runs into sooner or later. Most of us know how to fix it — add a header, change a config, ask the backend person to "do something about CORS." But how many of us actually understand what the browser is doing in the background, and why it's doing it?&lt;/p&gt;

&lt;p&gt;Let's go through it today, slowly, with a simple example.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's an "origin"?
&lt;/h2&gt;

&lt;p&gt;Before we get into CORS, there's one word we need to pin down: &lt;strong&gt;origin&lt;/strong&gt;. An origin is three things put together — the scheme, the host, and the port of a URL.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;https://example.com&lt;/code&gt; and &lt;code&gt;http://example.com&lt;/code&gt; are different origins because the scheme is different (https vs http). &lt;code&gt;https://example.com&lt;/code&gt; and &lt;code&gt;https://api.example.com&lt;/code&gt; are different because the host is different. And &lt;code&gt;https://example.com&lt;/code&gt; and &lt;code&gt;https://example.com:8080&lt;/code&gt; are different because the port is different. If any one of those three pieces changes, the browser treats it as a separate origin.&lt;/p&gt;

&lt;p&gt;This matters because the browser treats every origin as its own little sandbox. Whatever happens inside one origin is supposed to stay inside that origin.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser's default rule
&lt;/h2&gt;

&lt;p&gt;By default, JavaScript running on one origin is not allowed to read data from another origin. This rule has a name: &lt;strong&gt;same-origin policy&lt;/strong&gt;. Every modern browser ships with it built in, and it's the foundation that everything else here is built on.&lt;/p&gt;

&lt;p&gt;Without it, the web would be terrifying. Imagine you're logged into your bank in one tab. In another tab, you visit some random site. If same-origin policy didn't exist, that random site could just do this in the background:&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://yourbank.com/api/balance&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;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sendToAttacker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser would happily send the request, attach your bank cookies (because the cookies belong to the bank's domain), and the bank would return your balance to whoever asked. The random site's JavaScript would then read the response and send it anywhere it wanted. Every site you visit would be able to scrape every site you're logged into.&lt;/p&gt;

&lt;p&gt;Same-origin policy stops that. The browser still sends the request, but it refuses to let the random site's JavaScript read the response. The request happens; the response gets blocked from reaching the calling code.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what is CORS?
&lt;/h2&gt;

&lt;p&gt;CORS stands for &lt;strong&gt;Cross-Origin Resource Sharing&lt;/strong&gt;, and it's the mechanism a server uses to say "actually, it's fine, this other origin is allowed to read my response." It's an opt-in to relax same-origin policy in cases where cross-origin access is legitimate.&lt;/p&gt;

&lt;p&gt;The most common case is your own setup. Your frontend lives at &lt;code&gt;app.example.com&lt;/code&gt;, your API lives at &lt;code&gt;api.example.com&lt;/code&gt;. Different origins, so same-origin policy would block the frontend's calls by default. CORS is how your API tells the browser "yes, &lt;code&gt;app.example.com&lt;/code&gt; is allowed."&lt;/p&gt;

&lt;p&gt;How does the API tell the browser that? Through a response header:&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;Access-Control-Allow-Origin: https://app.example.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the browser sees this header on the response, it lets your JavaScript read it. Without it, the response is blocked and you see a CORS error in the console.&lt;/p&gt;

&lt;p&gt;One thing worth noticing here, because it confuses a lot of people: if your frontend and API are on the same origin during development — say both on &lt;code&gt;localhost:3000&lt;/code&gt; behind a Next.js rewrite — CORS doesn't kick in at all. The browser doesn't even check the headers, because nothing cross-origin is happening. CORS issues often only show up in production, when frontend and API are on different domains.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple example, end to end
&lt;/h2&gt;

&lt;p&gt;Let's walk through one fetch from start to finish.&lt;/p&gt;

&lt;p&gt;Your frontend at &lt;code&gt;https://app.example.com&lt;/code&gt; does:&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/users&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;Here's what actually happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The browser builds the request. It automatically adds a header: &lt;code&gt;Origin: https://app.example.com&lt;/code&gt;. This is the browser telling the server "this request is coming from app.example.com."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The browser sends the request to &lt;code&gt;api.example.com&lt;/code&gt;. The server receives it like any other request and processes it. It can read the database, run business logic, return data — whatever. From the server's point of view, this is a normal request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The server sends a response back, say with the user list as JSON.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The browser receives the response and checks the headers. Specifically, it looks at &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;. If that header is missing, or if its value doesn't match &lt;code&gt;https://app.example.com&lt;/code&gt; (or &lt;code&gt;*&lt;/code&gt;), the browser blocks the response. The JavaScript that called &lt;code&gt;fetch&lt;/code&gt; sees a CORS error, and the response body is never delivered to your code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the header is present and matches, the browser hands the response to the JavaScript, and your code continues normally.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the part that most explanations of CORS skip over: &lt;strong&gt;the server doesn't refuse the request. The browser does.&lt;/strong&gt; The server returned a perfectly fine response. The browser refused to hand it to the JavaScript because the server didn't include the right header saying "this origin is allowed."&lt;/p&gt;

&lt;p&gt;That's also why you can hit the same API from &lt;code&gt;curl&lt;/code&gt;, from Postman, or from a server-to-server call and it just works. CORS only exists in browsers. Other clients don't enforce same-origin policy, so there's nothing to block.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about complex requests? Preflight
&lt;/h2&gt;

&lt;p&gt;For "simple" requests — basically GETs and a few POSTs with safe content types — the browser sends the actual request directly and checks the response headers afterwards.&lt;/p&gt;

&lt;p&gt;But for anything more involved — a PUT, a DELETE, a request with a custom header like &lt;code&gt;Authorization&lt;/code&gt;, or a JSON-typed POST — the browser does an extra step first. It sends a &lt;strong&gt;preflight request&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A preflight is an &lt;code&gt;OPTIONS&lt;/code&gt; request to the same URL, sent before the real one. It looks like:&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="nf"&gt;OPTIONS&lt;/span&gt; &lt;span class="nn"&gt;/users&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://app.example.com&lt;/span&gt;
&lt;span class="na"&gt;Access-Control-Request-Method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DELETE&lt;/span&gt;
&lt;span class="na"&gt;Access-Control-Request-Headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Authorization, Content-Type&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser is essentially asking the server: "I'm about to send a DELETE with these headers. Are you okay with that?"&lt;/p&gt;

&lt;p&gt;The server responds with what it allows:&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="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;204&lt;/span&gt; &lt;span class="ne"&gt;No Content&lt;/span&gt;
&lt;span class="na"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://app.example.com&lt;/span&gt;
&lt;span class="na"&gt;Access-Control-Allow-Methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET, POST, PUT, DELETE&lt;/span&gt;
&lt;span class="na"&gt;Access-Control-Allow-Headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Authorization, Content-Type&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the browser is happy with the preflight response, it then sends the real request. If the preflight fails, the real request never goes out at all.&lt;/p&gt;

&lt;p&gt;This is why you sometimes see two requests in the network tab for what looks like a single fetch. The first is the preflight; the second is the actual call.&lt;/p&gt;

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

&lt;p&gt;There's one extra rule worth knowing, because it catches a lot of people off guard. If your frontend sends credentials with the request — cookies, an Authorization header, anything that identifies the user — like this:&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/me&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="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;include&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;Then a wildcard &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; is no longer enough. The browser refuses to deliver the response. The server has to name the specific origin it trusts, and add one more header:&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;Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason is security. If &lt;code&gt;*&lt;/code&gt; worked alongside credentials, any malicious site could fire authenticated requests to your API. The browser would attach the user's cookies (because cookies belong to the API's domain, not the calling page's). The server would see "any origin is allowed" and respond. The malicious site's JavaScript would then read the response — and every authenticated API on the web would be one fetch away from being scraped.&lt;/p&gt;

&lt;p&gt;So the rule is: &lt;code&gt;*&lt;/code&gt; is fine for genuinely public APIs that don't care who's calling. As soon as authentication enters the picture, the server has to be explicit about which origins it trusts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Here's the whole picture in one line: the browser blocks cross-origin reads by default (same-origin policy), and CORS is the mechanism a server uses to opt in to allowing them, through response headers like &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A few things worth remembering when you next see a CORS error in the console.&lt;/p&gt;

&lt;p&gt;The browser is the one doing the blocking, not the server. The server happily sends the response either way; the browser decides whether your JavaScript gets to see it. That's why CORS errors only happen in the browser, never in &lt;code&gt;curl&lt;/code&gt; or server-to-server calls.&lt;/p&gt;

&lt;p&gt;For non-simple requests, the browser sends an &lt;code&gt;OPTIONS&lt;/code&gt; preflight first to ask the server what's allowed. Two network requests for one fetch — that's why.&lt;/p&gt;

&lt;p&gt;And if credentials are involved, the wildcard stops working. The server has to name the origin explicitly and add &lt;code&gt;Access-Control-Allow-Credentials: true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;CORS isn't there to annoy developers. It's a security feature that protects users from sites that would otherwise be able to read their data from other sites. Once you see what it's protecting, the error messages start making more sense — and the fix usually comes down to figuring out exactly which header the browser wants to see, and making sure your server is sending it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>frontend</category>
      <category>security</category>
      <category>beginners</category>
    </item>
    <item>
      <title>When dev and prod disagree about your CSS</title>
      <dc:creator>Dipta</dc:creator>
      <pubDate>Fri, 15 May 2026 15:45:36 +0000</pubDate>
      <link>https://dev.to/dip_032d2fe1959e1990ddbb1/when-dev-and-prod-disagree-about-your-css-1ibe</link>
      <guid>https://dev.to/dip_032d2fe1959e1990ddbb1/when-dev-and-prod-disagree-about-your-css-1ibe</guid>
      <description>&lt;p&gt;This started right after we shipped a production release of our feature. One of our engineers was running through the change to confirm everything looked right, and the issue caught his eye — since it's a UI thing, it was easy to spot. The CSS width of some components was broken. In the development environment everything had been fine, working perfectly. But in production it wasn't. He reported it. So I started digging.&lt;/p&gt;

&lt;p&gt;The first instinct was the wrong one. I dropped an &lt;code&gt;!important&lt;/code&gt; on the rule, the width snapped back to what it should be, and I almost called it a day. Then I realized I'd just patched a symptom without understanding the cause — and a bug that behaves differently between dev and prod is exactly the kind of bug that comes back six months later in a different file. So I rolled the &lt;code&gt;!important&lt;/code&gt; back out and tried to actually figure out what was happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was colliding
&lt;/h2&gt;

&lt;p&gt;The element in question was a form label. It was being styled by two CSS Module rules at the same time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* shared component — shared.module.css */&lt;/span&gt;
&lt;span class="nc"&gt;.label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20.75rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* my page(comsumer) — mypage.module.css */&lt;/span&gt;
&lt;span class="nc"&gt;.fieldLabel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;11rem&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;The shared component does this internally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* styles.label is the shared component's own class from its CSS module. 
 * labelClassName is whatever class the caller passed in. 
 * mergeClassNames is just a helper that glues class strings together — same idea as clsx. 
 * The element ends up with both classes on it.
 */&lt;/span&gt; 
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;mergeClassNames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;labelClassName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;My Label&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So both classes were ending up on the same  element. The browser now has to decide which width wins. Two rules, both at &lt;code&gt;specificity (0,1,0)&lt;/code&gt; — one class each. A clean tie. And on a tie, CSS falls back to source order: whichever rule appears later in the final stylesheet wins.&lt;/p&gt;

&lt;p&gt;In dev mine appeared later, so &lt;code&gt;11rem&lt;/code&gt; won. In production the shared rule appeared later, so &lt;code&gt;20.75rem&lt;/code&gt; won. Same code. Different outcomes.&lt;/p&gt;

&lt;p&gt;The build had flipped the order on me.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I confirmed it
&lt;/h3&gt;

&lt;p&gt;I built and grepped the output to make sure I was right about which rule was winning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"11rem|20.75rem"&lt;/span&gt; out/_next/static/chunks/&amp;lt;the-file&amp;gt;.css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sure enough, both rules were sitting inside the same CSS chunk file. Mine came first, theirs came second. The tie-breaker said "later wins." Mine was "earlier." Mine lost.&lt;/p&gt;

&lt;p&gt;DevTools confirmed it too. Inspect the element in production, open the Styles panel, my rule was struck through. Hovering it, Chrome politely informed me it had been overridden by a rule with equivalent specificity that appeared later in the stylesheet.&lt;br&gt;
The browser was doing exactly what it was supposed to. I just hadn't been paying attention to which rule it considered "&lt;/p&gt;
&lt;h3&gt;
  
  
  Why dev and prod differ
&lt;/h3&gt;

&lt;p&gt;This part was the most interesting thing I learned from the whole bug.&lt;/p&gt;

&lt;p&gt;In development, Next.js doesn't bundle CSS. Each &lt;code&gt;.module.css&lt;/code&gt; is injected at runtime as its own &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tag, in the order JavaScript imports it. JavaScript imports go depth-first — so the children module's CSS ends up later in the DOM. Your override CSS, sitting at the leaf, naturally wins ties. It feels intuitive. It also lies, because nothing about that ordering is guaranteed.&lt;/p&gt;

&lt;p&gt;In production, next build runs Turbopack. It uses Lightning CSS to extract CSS Modules into chunk files in &lt;code&gt;out/_next/static/chunks/&lt;/code&gt;. Turbopack tries to follow JS import order when ordering CSS rules, but when a shared CSS module is reachable through multiple import paths (different pages, different parents), Turbopack has to pick one — and the chosen order may not match what dev's runtime-injected &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tags produce. &lt;/p&gt;

&lt;p&gt;For JavaScript that's fine — every module is independently identified and execution order is mostly its own concern. For CSS it's hostile. CSS rule order is part of the semantics. The bundler had just told me that part of the semantics wasn't mine to control.&lt;/p&gt;
&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;I'd seen people fix this kind of thing with &lt;code&gt;!important&lt;/code&gt; or with &lt;code&gt;.root .fieldLabel&lt;/code&gt; to bump specificity. Both work and both leave a mess — every future override has to fight the same fight. The cleaner fix turned out to be one CSS function I'd never used before: &lt;code&gt;:where()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I changed the shared component's rule to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:where&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;.label&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20.75rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&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;code&gt;:where(...)&lt;/code&gt; takes whatever selector you put inside and forces its specificity to zero. The shared component's default is now the weakest rule in the entire cascade. Anyone overriding it from the outside with even a single plain class wins automatically:&lt;/p&gt;

&lt;p&gt;Shared default: &lt;code&gt;0,0,0&lt;/code&gt;&lt;br&gt;
My override:    &lt;code&gt;0,1,0&lt;/code&gt;  ← always wins&lt;/p&gt;

&lt;p&gt;No source-order dependency. No tie to break. No reliance on whichever heuristic webpack happened to use this build.&lt;/p&gt;

&lt;p&gt;This is, in retrospect, what &lt;code&gt;:where()&lt;/code&gt; is for. It's the CSS equivalent of a default prop value — present if no one says otherwise, replaced freely the moment anyone does. If you maintain a shared UI component, every "default" style it ships should be wrapped this way. It costs you two extra characters and it saves every downstream caller from ever needing !important.&lt;/p&gt;

&lt;p&gt;A second knob, for defense&lt;/p&gt;

&lt;p&gt;I also turned on &lt;code&gt;experimental.cssChunking: 'strict'&lt;/code&gt; in &lt;code&gt;next.config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;cssChunking&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;strict&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;This doesn't help the within-chunk race we just fixed — order inside a chunk is webpack's call regardless. But it does help with a separate, related issue: when Next.js merges CSS files imported in different orders across modules, 'strict' tells it to stop. It's Next.js's official "stop reordering my CSS" option. Worth turning on if you've hit any version of this bug once.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I took away from it
&lt;/h3&gt;

&lt;p&gt;A bug that behaves differently in dev and prod is almost always a sign that something you thought was deterministic isn't. In this case it was source order — something I'd been quietly relying on for years without noticing.&lt;/p&gt;

&lt;p&gt;The way out, if there's a general lesson here, is to never write a fight between two equally-specific selectors. Pick a winner, on purpose. Either give your selector specificity that earns the win, or wrap the other one in &lt;code&gt;:where()&lt;/code&gt; to demote it. Letting the cascade decide for you by accident is fine until the day a bundler version changes and it's not.&lt;/p&gt;

&lt;p&gt;Two characters of &lt;code&gt;:where()&lt;/code&gt; would have saved me an afternoon.&lt;/p&gt;

</description>
      <category>css</category>
      <category>frontend</category>
      <category>nextjs</category>
      <category>react</category>
    </item>
  </channel>
</rss>
