<?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: chan</title>
    <description>The latest articles on DEV Community by chan (@didrod205).</description>
    <link>https://dev.to/didrod205</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%2F3969069%2F50aea184-cda6-481f-aec8-a1571983faf0.jpeg</url>
      <title>DEV Community: chan</title>
      <link>https://dev.to/didrod205</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/didrod205"/>
    <language>en</language>
    <item>
      <title>I rebuilt 4 dev tools so they stop uploading your data</title>
      <dc:creator>chan</dc:creator>
      <pubDate>Fri, 05 Jun 2026 04:08:33 +0000</pubDate>
      <link>https://dev.to/didrod205/i-rebuilt-3-dev-tools-so-they-stop-uploading-your-data-9pi</link>
      <guid>https://dev.to/didrod205/i-rebuilt-3-dev-tools-so-they-stop-uploading-your-data-9pi</guid>
      <description>&lt;p&gt;There's a quiet pattern in everyday dev work: the most convenient tool for a job&lt;br&gt;
is a website you paste sensitive data into.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Need to read a JWT? → paste a &lt;strong&gt;production token&lt;/strong&gt; into jwt.io.&lt;/li&gt;
&lt;li&gt;Checking a Content-Security-Policy? → paste your &lt;strong&gt;security config&lt;/strong&gt; into an online evaluator.&lt;/li&gt;
&lt;li&gt;Sharing a payload in a bug report? → paste &lt;strong&gt;customer PII&lt;/strong&gt; into an online redactor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each is genuinely useful — and each either &lt;strong&gt;uploads data you're trying to protect,&lt;br&gt;
or makes you deploy first just to check a config&lt;/strong&gt;. But none of these jobs needs a&lt;br&gt;
server: they're parsing, checksums, and rule-based analysis that run fine in your&lt;br&gt;
browser. So I rebuilt &lt;strong&gt;four&lt;/strong&gt; of them to run locally and upload nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. jwtlens — decode, audit &amp;amp; verify JWTs
&lt;/h2&gt;

&lt;p&gt;Decode the claims, run a security lint (&lt;code&gt;alg:none&lt;/code&gt;, missing &lt;code&gt;exp&lt;/code&gt;, HS↔RS&lt;br&gt;
alg-confusion), and &lt;strong&gt;verify the signature&lt;/strong&gt; client-side with the Web Crypto API —&lt;br&gt;
HS/RS/PS/ES, with your own secret/PEM/JWK.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://didrod205.github.io/jwtlens/" rel="noopener noreferrer"&gt;https://didrod205.github.io/jwtlens/&lt;/a&gt; · &lt;code&gt;npx jwtlens scan&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. csp-doctor — find the XSS holes in your CSP
&lt;/h2&gt;

&lt;p&gt;Paste a policy and it flags &lt;code&gt;'unsafe-inline'&lt;/code&gt;, wildcards, missing&lt;br&gt;
&lt;code&gt;object-src&lt;/code&gt;/&lt;code&gt;base-uri&lt;/code&gt;, and the allowlisted CDN hosts that silently &lt;em&gt;bypass&lt;/em&gt; CSP&lt;br&gt;
(JSONP / hosted AngularJS) — nonce/&lt;code&gt;strict-dynamic&lt;/code&gt;-aware, so a modern policy isn't&lt;br&gt;
flagged for nothing.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://didrod205.github.io/csp-doctor/" rel="noopener noreferrer"&gt;https://didrod205.github.io/csp-doctor/&lt;/a&gt; · &lt;code&gt;npx csp-doctor scan&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. scrubpii — make a payload safe to share
&lt;/h2&gt;

&lt;p&gt;Paste a JSON payload or log and get a redacted copy: emails, Luhn-valid cards,&lt;br&gt;
JWTs, API keys, IPs. Pseudonyms keep &lt;strong&gt;referential integrity&lt;/strong&gt; — the same value&lt;br&gt;
maps to the same alias everywhere, so the redacted data is still coherent.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://didrod205.github.io/scrubpii/" rel="noopener noreferrer"&gt;https://didrod205.github.io/scrubpii/&lt;/a&gt; · &lt;code&gt;cat payload.json | npx scrubpii redact&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. cookie-doctor — lint your Set-Cookie headers
&lt;/h2&gt;

&lt;p&gt;Paste a &lt;code&gt;Set-Cookie&lt;/code&gt; (or a &lt;code&gt;curl -I&lt;/code&gt; response) and it flags missing&lt;br&gt;
&lt;code&gt;HttpOnly&lt;/code&gt;/&lt;code&gt;Secure&lt;/code&gt;/&lt;code&gt;SameSite&lt;/code&gt;, &lt;code&gt;SameSite=None&lt;/code&gt; without &lt;code&gt;Secure&lt;/code&gt;, and the&lt;br&gt;
&lt;code&gt;__Host-&lt;/code&gt;/&lt;code&gt;__Secure-&lt;/code&gt; &lt;strong&gt;prefix rules&lt;/strong&gt; — the violations that make a browser&lt;br&gt;
&lt;em&gt;silently drop&lt;/em&gt; your cookie, so logins mysteriously stop sticking.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://didrod205.github.io/cookie-doctor/" rel="noopener noreferrer"&gt;https://didrod205.github.io/cookie-doctor/&lt;/a&gt; · &lt;code&gt;curl -sI url | npx cookie-doctor scan&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How they're built (the part that makes "local-first" honest)
&lt;/h2&gt;

&lt;p&gt;Each is a small TypeScript package with a &lt;strong&gt;pure, zero-dependency core&lt;/strong&gt; that powers&lt;br&gt;
both a CLI and the browser playground — same engine, same results. The CLI lets you&lt;br&gt;
gate any of this in CI; the playground is just that core compiled for the web. The&lt;br&gt;
only "server-ish" bit, JWT signature verification, runs on &lt;code&gt;crypto.subtle&lt;/code&gt; in the&lt;br&gt;
browser and &lt;code&gt;node:crypto&lt;/code&gt; in the CLI (fun detail: JWS ECDSA signatures are raw&lt;br&gt;
P1363, which WebCrypto wants natively but Node needs told explicitly).&lt;/p&gt;

&lt;p&gt;All MIT, all on npm. If you maintain something similar, the takeaway is simply:&lt;br&gt;
&lt;strong&gt;a lot of "paste it into our website" tools don't need to be websites.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Which dev tool do you wish ran locally? I'm collecting ideas.&lt;/p&gt;

</description>
      <category>security</category>
      <category>privacy</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>You're probably leaking production tokens into jwt.io</title>
      <dc:creator>chan</dc:creator>
      <pubDate>Fri, 05 Jun 2026 03:48:18 +0000</pubDate>
      <link>https://dev.to/didrod205/youre-probably-leaking-production-tokens-into-jwtio-3e1c</link>
      <guid>https://dev.to/didrod205/youre-probably-leaking-production-tokens-into-jwtio-3e1c</guid>
      <description>&lt;p&gt;You hit a 401. You grab the JWT from a log line, drop it into &lt;strong&gt;jwt.io&lt;/strong&gt; to read&lt;br&gt;
the claims, and move on. I did this for years — until I realized that token was&lt;br&gt;
often a &lt;em&gt;still-valid production credential&lt;/em&gt;, and I'd just handed it to a&lt;br&gt;
third-party website.&lt;/p&gt;

&lt;p&gt;Even when a decoder swears "it stays in your browser," do you really want to take&lt;br&gt;
that on faith for a prod token? You don't have to. Everything jwt.io does —&lt;br&gt;
decode, inspect, &lt;strong&gt;verify the signature&lt;/strong&gt; — can run entirely on your own machine.&lt;/p&gt;

&lt;p&gt;Here's what's worth knowing, and a tiny tool that does it locally.&lt;/p&gt;
&lt;h2&gt;
  
  
  A JWT is three base64url chunks
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;header.payload.signature&lt;/code&gt;. Decoding is just base64url + &lt;code&gt;JSON.parse&lt;/code&gt; — no secret&lt;br&gt;
needed, which is exactly why "decode" should never require uploading anything.&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/-/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/_/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&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;The interesting part is everything &lt;em&gt;around&lt;/em&gt; the decode.&lt;/p&gt;

&lt;h2&gt;
  
  
  The footguns a decoder should flag
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;alg: none&lt;/code&gt;.&lt;/strong&gt; An "unsecured" JWT has no signature. If your server ever accepts&lt;br&gt;
&lt;code&gt;none&lt;/code&gt;, anyone can forge any token. This is the first thing to check, and it's a&lt;br&gt;
one-line lint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HS↔RS algorithm confusion.&lt;/strong&gt; A server that verifies with the &lt;em&gt;algorithm named in&lt;br&gt;
the token&lt;/em&gt; (instead of a pinned one) can be tricked: an attacker takes your RSA&lt;br&gt;
&lt;strong&gt;public&lt;/strong&gt; key, signs a token with &lt;code&gt;HS256&lt;/code&gt; using that public key as the HMAC&lt;br&gt;
secret, and your server happily verifies it. The fix is to pin the expected alg —&lt;br&gt;
but a linter can at least warn when a token uses a symmetric alg.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expiry hygiene.&lt;/strong&gt; No &lt;code&gt;exp&lt;/code&gt; means a leaked token is valid forever. A 30-day access&lt;br&gt;
token means a 30-day blast radius. Missing &lt;code&gt;aud&lt;/code&gt;/&lt;code&gt;iss&lt;/code&gt; means the token isn't scoped&lt;br&gt;
to your service.&lt;/p&gt;
&lt;h2&gt;
  
  
  Verifying a signature — in the browser
&lt;/h2&gt;

&lt;p&gt;This is the part people assume needs a server. It doesn't. The Web Crypto API&lt;br&gt;
(&lt;code&gt;crypto.subtle&lt;/code&gt;) verifies HS/RS/PS/ES signatures client-side.&lt;/p&gt;

&lt;p&gt;One gotcha worth the price of admission: &lt;strong&gt;ECDSA (ES256) signatures.&lt;/strong&gt; JWS encodes&lt;br&gt;
them as raw &lt;code&gt;r || s&lt;/code&gt; (IEEE-P1363). Browser WebCrypto's ECDSA wants exactly that&lt;br&gt;
format — but Node's &lt;code&gt;crypto&lt;/code&gt; defaults to DER and needs &lt;code&gt;dsaEncoding: 'ieee-p1363'&lt;/code&gt;.&lt;br&gt;
Same math, two different serializations, and a classic source of "why won't this&lt;br&gt;
verify" bugs.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ECDSA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// imported from your PEM/JWK&lt;/span&gt;
  &lt;span class="nx"&gt;rawSignatureBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// the P1363 bytes straight from the JWT&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&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;h&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;p&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No network. No upload. Your key never leaves the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool
&lt;/h2&gt;

&lt;p&gt;I packaged all of this into &lt;strong&gt;jwtlens&lt;/strong&gt; — decode, a security lint, and offline&lt;br&gt;
verification (HS/RS/PS/ES with your own secret, PEM, or JWK).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser playground&lt;/strong&gt; (nothing uploaded): &lt;a href="https://didrod205.github.io/jwtlens/" rel="noopener noreferrer"&gt;https://didrod205.github.io/jwtlens/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI&lt;/strong&gt; for CI / scripts: &lt;code&gt;npx jwtlens scan "$TOKEN"&lt;/code&gt; (or &lt;code&gt;verify&lt;/code&gt; with a key)&lt;/li&gt;
&lt;li&gt;MIT, zero runtime deps in the core: &lt;a href="https://github.com/didrod205/jwtlens" rel="noopener noreferrer"&gt;https://github.com/didrod205/jwtlens&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's deliberately small and boring — a linter and a verifier, not a service. The&lt;br&gt;
point is that none of this &lt;em&gt;needs&lt;/em&gt; to be a service.&lt;/p&gt;

&lt;p&gt;If you maintain an auth service, the most useful takeaway isn't the tool — it's:&lt;br&gt;
&lt;strong&gt;pin your expected algorithm, set short &lt;code&gt;exp&lt;/code&gt;s, and stop pasting live tokens into&lt;br&gt;
websites.&lt;/strong&gt; The tool just makes the last one easy.&lt;/p&gt;

&lt;p&gt;What would you want a JWT linter to catch that I haven't covered? I'm collecting&lt;br&gt;
rules.&lt;/p&gt;

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