<?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: sachin k</title>
    <description>The latest articles on DEV Community by sachin k (@sachinpk).</description>
    <link>https://dev.to/sachinpk</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%2F4081767%2Fc3bd1fb5-792c-46d7-9bf3-0c74a2511f00.png</url>
      <title>DEV Community: sachin k</title>
      <link>https://dev.to/sachinpk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sachinpk"/>
    <language>en</language>
    <item>
      <title>A Package You Use Was Just Compromised. Now What?</title>
      <dc:creator>sachin k</dc:creator>
      <pubDate>Thu, 20 Aug 2026 13:14:19 +0000</pubDate>
      <link>https://dev.to/sachinpk/a-package-you-use-was-just-compromised-now-what-41n1</link>
      <guid>https://dev.to/sachinpk/a-package-you-use-was-just-compromised-now-what-41n1</guid>
      <description>&lt;p&gt;There are plenty of good posts about preventing supply-chain attacks: &lt;code&gt;**ignore-scripts=true, minimumReleaseAge,**&lt;/code&gt; frozen lockfiles, provenance checks. Read them. Apply them.&lt;/p&gt;

&lt;p&gt;But all of those posts end at the same cliff. What happens when prevention fails?&lt;/p&gt;

&lt;p&gt;You open Slack and see: "lib-x versions 4.2.1 to 4.2.3 were compromised, malicious postinstall, rotate your creds." Your heart rate doubles. What do you actually type into your terminal right now?&lt;/p&gt;

&lt;p&gt;Nobody writes that post, so I did. A 60-minute playbook, in order, with the exact commands. It assumes npm/pnpm/yarn or pip/uv, but the logic ports anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minute 0-10: Answer one question first. Did I ever resolve the bad version?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Don't rotate anything yet. Don't panic-delete node_modules. First establish whether you were ever exposed, because "we depend on lib-x" and "we installed lib-x 4.2.2" are very different incidents.&lt;/p&gt;

&lt;p&gt;Check what's installed right now&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="c"&gt;# npm / pnpm / yarn: shows every resolved copy, including transitive&lt;/span&gt;
npm &lt;span class="nb"&gt;ls &lt;/span&gt;lib-x &lt;span class="nt"&gt;--all&lt;/span&gt;
pnpm why lib-x
yarn why lib-x&lt;span class="sb"&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 shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Python&lt;/span&gt;
&lt;span class="sb"&gt;`&lt;/span&gt;pip show lib-x
uv pip list | &lt;span class="nb"&gt;grep &lt;/span&gt;lib-x&lt;span class="sb"&gt;`&lt;/span&gt;
Check the lockfile, including its &lt;span class="nb"&gt;history&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your current lockfile might be clean while last Tuesday's wasn't. The exposure window is every commit where the lockfile referenced a bad version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="c"&gt;# Every lockfile commit that ever mentioned the package&lt;/span&gt;
git log &lt;span class="nt"&gt;--follow&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; package-lock.json | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-B2&lt;/span&gt; &lt;span class="nt"&gt;-A2&lt;/span&gt; &lt;span class="s1"&gt;'"lib-x"'&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Faster: which commits introduced/removed the bad version string
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="s1"&gt;'4.2.2'&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; package-lock.json pnpm-lock.yaml yarn.lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Python equivalents
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="sb"&gt;`&lt;/span&gt;git log &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="s1"&gt;'lib-x==4.2.2'&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; requirements.txt uv.lock poetry.lock&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't forget branches and open PRs&lt;/p&gt;

&lt;p&gt;A Renovate or Dependabot PR sitting open with the bad version has already been installed by CI on every push to that branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="c"&gt;# Search the bad version across ALL branches&lt;/span&gt;
git &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'4.2.2'&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;git &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="nt"&gt;-each-ref&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'%(refname)'&lt;/span&gt; refs/remotes&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'*lock*'&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Decision point.&lt;/strong&gt; If the bad version never appeared anywhere, you're done. Write two sentences in your incident channel, add the prevention controls, go back to work. If it appears in any lockfile, on any branch, at any point in the disclosed window, keep reading. You are now in incident mode.&lt;/p&gt;

&lt;p&gt;Minute 10-20: Could the payload have executed?&lt;/p&gt;

&lt;p&gt;Compromised is not the same as executed. Figure out which class of payload you're dealing with. Advisories usually say.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Class A:&lt;/strong&gt; install-time payload (preinstall/postinstall, setup.py). It ran on every machine that installed it. Every dev laptop, every CI runner, every Docker build. If you run with ignore-scripts=true (or pnpm's default script blocking, or Bun), your laptops may be fine. Check CI separately though, because CI configs often differ from local ones.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="c"&gt;# Did this environment allow scripts?&lt;/span&gt;
npm config get ignore-scripts        &lt;span class="c"&gt;# want: true&lt;/span&gt;
pnpm config get ignore-scripts&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Class B:&lt;/code&gt; runtime payload (malicious code inside the module itself). Installing it was harmless. Importing it was not. Now the question is which processes loaded it: your app in prod, your test suite in CI, a build script?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="c"&gt;# Was it actually imported anywhere, or just present in the tree?&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"require(['&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]lib-x"&lt;/span&gt; src/ scripts/
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"from ['&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]lib-x"&lt;/span&gt; src/
&lt;span class="c"&gt;# Python&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"import lib_x&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;from lib_x"&lt;/span&gt; .&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write down your blast radius in one line. Something like: "Class A payload, scripts enabled in CI but not laptops, CI ran 14 installs between Aug 12 and Aug 18." Everything after this is scoped by that sentence.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Minute 20-35: Contain. Rotate what the payload could reach.&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
This is the step people do in the wrong order. Rotate before cleanup, because your cleanup commits will trigger CI, and if CI secrets are burned you're re-exposing fresh code to a compromised environment.&lt;/p&gt;

&lt;p&gt;Recent real-world payloads (the Shai-Hulud-style worms) go straight for tokens. Assume the payload harvested every credential readable from the environment it ran in.&lt;/p&gt;

&lt;p&gt;If it ran in CI:&lt;br&gt;
All repo/org secrets in that CI system (GitHub Actions secrets, GitLab variables)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud credentials.&lt;/strong&gt; Prefer OIDC federation over static keys going forward, but rotate any static AWS_&lt;em&gt;/GCP_&lt;/em&gt; keys now.&lt;br&gt;
NPM_TOKEN / PYPI_TOKEN. Top priority if you publish packages. This is how one compromise becomes a worm: your token publishes the next infected version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="c"&gt;# npm: list and revoke tokens&lt;/span&gt;
npm token list
npm token revoke &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sb"&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 shell"&gt;&lt;code&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="c"&gt;# GitHub: check for tokens/keys you didn't create&lt;/span&gt;
gh api /user/keys
gh auth status&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;If it ran on laptops:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;~/.npmrc and ~/.pypirc tokens&lt;br&gt;
SSH keys, gh CLI tokens, long-lived cloud creds in ~/.aws/credentials&lt;br&gt;
.env files in project directories. These are exactly what the payloads grab.&lt;/p&gt;

&lt;p&gt;Also check for persistence. Several npm worms added malicious GitHub Actions workflows or new repos using stolen tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="c"&gt;# Recently created/modified workflows across your org&lt;/span&gt;
gh api &lt;span class="s2"&gt;"search/code?q=org:YOUR_ORG+path:.github/workflows&amp;amp;sort=indexed"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'.items[].repository.full_name'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;

&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="c"&gt;# Repos created during the exposure window&lt;/span&gt;
gh repo list YOUR_ORG &lt;span class="nt"&gt;--limit&lt;/span&gt; 200 &lt;span class="nt"&gt;--json&lt;/span&gt; name,createdAt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'.[] | select(.createdAt &amp;gt; "2026-08-12")'&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;*&lt;em&gt;Minute 35-50: Eradicate the version, and every cache holding it&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Fixing package.json is the easy 10%. The tarball is also sitting in caches that will happily re-serve it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="sb"&gt;`&lt;/span&gt;1. Force a safe version everywhere, including transitive deps
jsonc
// package.json &lt;span class="o"&gt;(&lt;/span&gt;npm&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="s2"&gt;"overrides"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"lib-x"&lt;/span&gt;: &lt;span class="s2"&gt;"4.2.0"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

// pnpm: pnpm-workspace.yaml &lt;span class="o"&gt;(&lt;/span&gt;or package.json pnpm.overrides&lt;span class="o"&gt;)&lt;/span&gt;
// overrides:
//   lib-x: 4.2.0

// yarn
&lt;span class="s2"&gt;"resolutions"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"lib-x"&lt;/span&gt;: &lt;span class="s2"&gt;"4.2.0"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
bash
&lt;span class="c"&gt;# Python: pin explicitly, even if it's transitive&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"lib-x==4.2.0"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; requirements.txt   &lt;span class="c"&gt;# or constraints.txt&lt;/span&gt;
uv lock &lt;span class="nt"&gt;--upgrade-package&lt;/span&gt; lib-x&lt;span class="o"&gt;==&lt;/span&gt;4.2.0&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then regenerate and diff the lockfile by hand before committing. You're looking for the bad version disappearing and nothing suspicious appearing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Purge every cache layer
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="c"&gt;# Local&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; node_modules
npm cache clean &lt;span class="nt"&gt;--force&lt;/span&gt;
pnpm store prune
yarn cache clean
pip cache purge &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; uv cache clean
&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="c"&gt;# CI caches. This is the one everyone forgets.&lt;/span&gt;
&lt;span class="c"&gt;# GitHub Actions: delete caches so restored node_modules can't resurrect the payload&lt;/span&gt;
gh cache list
gh cache delete &lt;span class="nt"&gt;--all&lt;/span&gt;

&lt;span class="c"&gt;# **Docker:** layer caches can pin the bad tarball indefinitely&lt;/span&gt;
docker builder prune &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run a registry proxy (Artifactory, Verdaccio, Nexus), purge the bad version there too. Otherwise every future install in your org re-downloads it from your own mirror.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;3. Rebuild and redeploy anything built in the window&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Any artifact (Docker image, Lambda zip, frontend bundle) built while the bad version was resolvable is suspect. Rebuild from the fixed lockfile, redeploy, and if you keep image digests, note which digests were built in the window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minute 50-60: Verify, then write it down&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="c"&gt;# Confirm the bad version is gone from the tree&lt;/span&gt;
npm &lt;span class="nb"&gt;ls &lt;/span&gt;lib-x &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;span class="c"&gt;# Confirm the lockfile can't drift&lt;/span&gt;
npm ci        &lt;span class="c"&gt;# or: pnpm install --frozen-lockfile&lt;/span&gt;
&lt;span class="c"&gt;# Confirm integrity/signatures on what you now have&lt;/span&gt;
npm audit signatures&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then write a short postmortem. Five lines, in the incident channel, today:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exposure window:&lt;/strong&gt; first commit to fix commit, with timestamps&lt;br&gt;
Blast radius: which environments installed it, whether scripts or imports executed&lt;br&gt;
What was rotated (and what deliberately wasn't, and why)&lt;br&gt;
Caches purged, artifacts rebuilt&lt;br&gt;
The prevention gap that let it in, with one linked follow-up ticket&lt;/p&gt;

&lt;p&gt;That last line is where all those prevention articles finally become relevant: ignore-scripts=true, a release-age cooldown (minimum-release-age in npm 11.10+, on by default in pnpm 11, npmMinimalAgeGate in Yarn 4.10+), frozen-lockfile installs in CI, and OIDC instead of static cloud keys. Prevention posts tell you to do these things. An incident tells you which one you actually needed.&lt;/p&gt;

&lt;p&gt;The 10-second version to bookmark&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;`1. git log -S '&amp;lt;bad-version&amp;gt;' -- &amp;lt;lockfiles&amp;gt;     -&amp;gt; was I ever exposed? (all branches!)
2. Install-time or runtime payload?              -&amp;gt; scope: laptops vs CI vs prod
3. Rotate: publish tokens &amp;gt; CI secrets &amp;gt; cloud keys &amp;gt; laptop creds
4. Check persistence: new workflows, new repos, new tokens
5. overrides/resolutions pin + purge npm/pnpm/CI/Docker caches
6. Rebuild artifacts from the window; redeploy
7. npm ci + npm audit signatures                 -&amp;gt; verify
8. Five-line postmortem + one prevention ticket`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prevention articles get written because prevention is comfortable. Incident response gets skipped because it's the part where you were already wrong. The window between "compromise published" and "compromise detected" will never be zero, which means this playbook isn't a nice-to-have. It's the half of supply-chain security we all stopped writing about.&lt;/p&gt;

&lt;p&gt;If you've lived through one of these, drop your war story in the comments. Especially the cache layer you forgot.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>cybersecurity</category>
      <category>npm</category>
    </item>
    <item>
      <title>Cybersecurity from Zero to Hero #4: Passwords and Hashing, or Why a Good Site Cannot Tell You Your Own Password</title>
      <dc:creator>sachin k</dc:creator>
      <pubDate>Thu, 20 Aug 2026 13:00:38 +0000</pubDate>
      <link>https://dev.to/sachinpk/cybersecurity-from-zero-to-hero-4-passwords-and-hashing-or-why-a-good-site-cannot-tell-you-your-2oe2</link>
      <guid>https://dev.to/sachinpk/cybersecurity-from-zero-to-hero-4-passwords-and-hashing-or-why-a-good-site-cannot-tell-you-your-2oe2</guid>
      <description>&lt;p&gt;Welcome back to Cybersecurity from Zero to Hero, the series where I learn security from scratch and write it down while the confusion is still fresh. In post three we separated authentication from authorization. Today we go inside the most common authentication factor on earth: the password, and the strange one way math that protects it.&lt;/p&gt;

&lt;p&gt;Here is the question that unlocked this whole topic for me. When you click Forgot Password, why does every decent site send you a reset link instead of just telling you your password? The answer is not policy. The answer is that a well built site literally does not know your password. And that is exactly how it should be.&lt;/p&gt;

&lt;p&gt;🥣 &lt;strong&gt;Hashing: the one way blender&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you create an account, a good site never stores the password you typed. Instead it runs the password through a hash function and stores only the result.&lt;/p&gt;

&lt;p&gt;A hash function is like a blender. Drop in an apple and you get a very specific smoothie. Drop in the same apple tomorrow and you get the same smoothie. But no power on earth un-blends the smoothie back into the apple. That is the whole trick: same input always gives the same output, and the process only runs one direction.&lt;/p&gt;

&lt;p&gt;So the login flow works like this. You type your password. The site blends it. It compares your fresh smoothie to the smoothie it stored on day one. Match means you knew the password. The site verified your password without ever storing it.&lt;/p&gt;

&lt;p&gt;Now the Forgot Password mystery solves itself. The site cannot email you your password because all it has is the smoothie. The only honest option is a reset link that lets you make a new one.&lt;/p&gt;

&lt;p&gt;💻 &lt;strong&gt;What I actually did: hashing my first strings in Python&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the first hands on in the series that uses code, and it needs nothing but Python's standard library. Fifteen lines, no installs:&lt;/p&gt;

&lt;p&gt;`python&lt;br&gt;
import hashlib&lt;/p&gt;

&lt;p&gt;def hash_it(text):&lt;br&gt;
    return hashlib.sha256(text.encode()).hexdigest()&lt;/p&gt;

&lt;p&gt;print(hash_it("hello"))&lt;br&gt;
print(hash_it("hello"))&lt;br&gt;
print(hash_it("Hello"))`&lt;/p&gt;

&lt;p&gt;Three things jumped out the moment I ran it.&lt;/p&gt;

&lt;p&gt;Same input, same output. The first two lines print identical hashes. Deterministic, as promised.&lt;/p&gt;

&lt;p&gt;Tiny change, chaos. Capitalizing one letter produced a hash with no resemblance to the first. This is called the avalanche effect, and it is why hashes make great tamper detectors. It is the same property we used in post one when we verified a download with a checksum. Different purpose, same math.&lt;/p&gt;

&lt;p&gt;Any size in, same size out. I hashed a single letter and then a whole paragraph. Both came out as 64 hex characters. A hash is a fingerprint, not a container.&lt;/p&gt;

&lt;p&gt;If you followed my Python series, this is where the two tracks officially shake hands. If not, you can still run those five lines in any online Python playground and see it yourself.&lt;/p&gt;

&lt;p&gt;🔓 &lt;strong&gt;So how do passwords get cracked at all?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If hashing cannot be reversed, why do we keep hearing about cracked password databases? Because attackers do not reverse the blender. They run the blender forward, billions of times.&lt;/p&gt;

&lt;p&gt;Take every common password, hash each one, and compare the results against the stolen database. Whenever a hash matches, you have found someone's password. Precomputed versions of this are called rainbow tables, and against common passwords they work depressingly well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is exactly why two defenses exist:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Salting.&lt;/strong&gt; The site adds a random string to your password before hashing, and stores the salt alongside the hash. Now two users with the same password get different hashes, and precomputed tables become useless. Every guess has to be recomputed per user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slow hashing.&lt;/strong&gt; SHA 256 is designed to be fast, which is great for checksums and terrible for passwords, because fast for you is fast for attackers. Password storage uses deliberately slow functions like bcrypt or argon2 that turn billions of guesses per second into thousands. My demo above uses SHA 256 because it ships with Python and shows the concepts perfectly, but hear this clearly: real systems must never store passwords with plain SHA 256.&lt;/p&gt;

&lt;p&gt;And the math behind length beats everything. Each extra character multiplies the guessing work. This is why every serious guide says the same boring thing: long unique passwords in a password manager beat clever short ones every time. In post two my worst risk score was password reuse. This post is the reason it scored so high: one cracked database anywhere unlocks every account where I reused that password.&lt;/p&gt;

&lt;p&gt;🤔 &lt;strong&gt;What confused me today&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If hashes are one way, how does my password manager show me my passwords? Because a password manager does not hash, it encrypts. Encryption is a two way door that opens with a key, in this case my master password. Hashing is for verifying, encryption is for retrieving. Two different tools that beginner me had filed in the same drawer.&lt;/p&gt;

&lt;p&gt;What are collisions? In theory two different inputs could produce the same hash, since infinite inputs map to fixed size outputs. For SHA-256, nobody has ever found one, and the numbers involved are absurd. Older functions like MD5 have real, demonstrated collisions, which is why you still see MD5 in tutorials but never in serious modern systems.&lt;/p&gt;

&lt;p&gt;⏭️ &lt;strong&gt;Next in this series&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Post 5 covers multi-factor authentication, including what actually happens inside that six-digit code app and why the codes keep working with your phone in airplane mode. The hands on part sets one up and proves that offline trick. Follow the series so you do not miss it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cybersecurity</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cybersecurity from Zero to Hero #3: Authentication vs Authorization, or Why the Bouncer Checks Your ID Twice</title>
      <dc:creator>sachin k</dc:creator>
      <pubDate>Thu, 20 Aug 2026 12:56:12 +0000</pubDate>
      <link>https://dev.to/sachinpk/cybersecurity-from-zero-to-hero-3-authentication-vs-authorization-or-why-the-bouncer-checks-your-2g0k</link>
      <guid>https://dev.to/sachinpk/cybersecurity-from-zero-to-hero-3-authentication-vs-authorization-or-why-the-bouncer-checks-your-2g0k</guid>
      <description>&lt;p&gt;Welcome back to Cybersecurity from Zero to Hero, the series where I learn security from scratch and write it down while the confusion is still fresh. In post two we untangled threats, vulnerabilities and risk. Today we tackle two words that even experienced developers mix up in conversation: authentication and authorization.&lt;/p&gt;

&lt;p&gt;They sound alike, they often happen within the same second, and they get abbreviated to the nearly identical authn and authz. But they answer two completely different questions, and confusing them has caused real breaches.&lt;/p&gt;

&lt;p&gt;🎟️ &lt;strong&gt;The nightclub analogy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Picture a nightclub with a bouncer and a VIP area.&lt;/p&gt;

&lt;p&gt;Authentication asks: who are you, and can you prove it? That is the bouncer at the front door checking your ID against your face. You claim an identity, then you prove it. If the proof fails, you never get inside at all.&lt;/p&gt;

&lt;p&gt;Authorization asks: what are you allowed to do now that you are inside? Your regular ticket gets you the main floor. It does not get you into the VIP area, and it definitely does not get you behind the bar pouring drinks. The second bouncer at the VIP rope is not asking who you are again. He is asking what your ticket permits.&lt;/p&gt;

&lt;p&gt;One sentence to remember forever: authentication is about identity, authorization is about permission. You cannot have the second without the first, but proving who you are never automatically entitles you to everything.&lt;/p&gt;

&lt;p&gt;💻 &lt;strong&gt;Translating it to computers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Authentication is the login. Username plus password, a fingerprint, a code from your authenticator app. The system verifies you are who you claim to be. We will spend two full posts on this soon, because passwords and multi-factor authentication each deserve their own deep dive.&lt;/p&gt;

&lt;p&gt;Authorization is everything after the login. Can this account read that file? Can it delete other users? Can it see the admin dashboard? In real systems this runs through things like roles and permissions. A regular user and an admin might log in through the same screen, and the difference between them is pure authorization.&lt;/p&gt;

&lt;p&gt;Here is why the distinction matters for security. Some of the ugliest vulnerabilities are authorization failures, not authentication failures. The attacker logs in as a completely legitimate low level user, then reaches data or actions that should have been off limits. Nothing about the login was broken. The system simply forgot to check the ticket at the VIP rope. In the web security world this shows up as broken access control, and it currently sits at the very top of the OWASP Top 10 list of web application risks. We will meet it hands on in phase four of this series.&lt;/p&gt;

&lt;p&gt;A tiny example of the difference in code terms. Changing a URL from /account/1234 to /account/1235 and seeing someone else's data is an authorization failure. Guessing someone's password is an authentication failure. Same damage, totally different broken door.&lt;/p&gt;

&lt;p&gt;📝 &lt;strong&gt;What I actually did: mapping a real login flow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The hands on exercise this time needs nothing but a browser. I picked a site I use daily and wrote down every identity and permission step from arrival to action. Try it with any site you like. Mine looked like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt;, the claim. I typed my email. That is me claiming an identity. No proof yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt;, the proof. I typed my password. First authentication factor: something I know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt;, more proof. The site asked for a six digit code from my phone. Second factor: something I have. Two proofs, one identity. This is multi-factor authentication, coming up in post 5.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4&lt;/strong&gt;, the session. After login the site set a cookie in my browser. I found it in DevTools under Application, then Cookies. That cookie is my wristband. For the rest of the visit the site checks the wristband, not my ID. This also explains something I never understood before: logging out just destroys the wristband. My password did not change. The session ended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5&lt;/strong&gt;, the permissions. I opened my account settings and saw sections I can use, and I remembered that an admin of the same site sees panels I do not. Same login flow, different tickets. That gap between what I see and what an admin sees is authorization, drawn as a user interface.&lt;/p&gt;

&lt;p&gt;Five steps, and the first three were authentication while the last two were session and authorization. Once you map one site this way, you start seeing the pattern absolutely everywhere.&lt;/p&gt;

&lt;p&gt;🤔 &lt;strong&gt;What confused me today&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Is the session cookie authentication or authorization? It puzzled me because it feels like both. The cleanest answer I found: the cookie is proof of an already completed authentication, a temporary stand in for your ID so you do not retype your password on every click. The system still runs authorization checks against your permissions on every request. The wristband proves you were checked at the door. It does not decide what you may touch.&lt;/p&gt;

&lt;p&gt;Why do sites make me re-enter my password before sensitive actions like changing email? Now I can name it: that is reauthentication. The site trusts the wristband for browsing, but for dangerous actions it wants fresh proof of identity, in case someone walked up to my unlocked laptop. Annoying, and correct. It is the same tension we met in post one between availability and confidentiality.&lt;/p&gt;

&lt;p&gt;⏭️ &lt;strong&gt;Next in this series&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Post 4 covers passwords and hashing, including why a well built site literally cannot tell you your own password, and why that is exactly how it should be. The hands on part builds a tiny hashing demo in Python. Follow the series so you do not miss it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cybersecurity</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cybersecurity from Zero to Hero #2: Threats, Vulnerabilities and Risk Are Not the Same Thing</title>
      <dc:creator>sachin k</dc:creator>
      <pubDate>Tue, 18 Aug 2026 11:14:57 +0000</pubDate>
      <link>https://dev.to/sachinpk/cybersecurity-from-zero-to-hero-2-threats-vulnerabilities-and-risk-are-not-the-same-thing-55n6</link>
      <guid>https://dev.to/sachinpk/cybersecurity-from-zero-to-hero-2-threats-vulnerabilities-and-risk-are-not-the-same-thing-55n6</guid>
      <description>&lt;p&gt;Welcome back to Cybersecurity from Zero to Hero, the series where I learn security from scratch and write it down while the confusion is still fresh. In post one we met the CIA Triad, the three promises every security control exists to protect. Today we learn the three words that describe how those promises get broken.&lt;/p&gt;

&lt;p&gt;Here is the problem. Security articles throw around threat, vulnerability and risk as if they were synonyms. They are not. Once I finally separated them, every security article I read became twice as clear, and I could suddenly follow conversations that used to sound like noise.&lt;/p&gt;

&lt;p&gt;🏠 &lt;strong&gt;The house analogy that made it stick&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine your house.&lt;/p&gt;

&lt;p&gt;A threat is anything out there that could cause harm. The burglar walking through your neighborhood. The storm forming offshore. The threat exists whether or not your house has any weaknesses. You do not control threats. They are simply out there.&lt;/p&gt;

&lt;p&gt;A vulnerability is a weakness in your house. The broken lock on the back door. The window that does not close properly. A vulnerability by itself harms nothing. A broken lock in a neighborhood with zero burglars is just a broken lock.&lt;/p&gt;

&lt;p&gt;Risk is what happens when the two meet. It is the likelihood that a threat actually exploits a vulnerability, multiplied by how bad it would be. Burglar in the area, plus broken lock, plus your laptop and passport sitting inside: now you have real risk.&lt;/p&gt;

&lt;p&gt;The formula you will see everywhere in this field:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Risk = Threat x Vulnerability x Impact&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
Kill any one of the three and the risk collapses. You cannot remove the burglar, but you can fix the lock, or move the passport to a safe. That is what security work actually is: reducing risk by attacking the parts you control.&lt;/p&gt;

&lt;p&gt;💻 &lt;strong&gt;Translating it to computers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same three words, now in tech form:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Threats&lt;/strong&gt;: ransomware gangs, phishing crews, a disgruntled employee, even a hurricane heading for a data center. Remember post one: not every threat is a hacker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vulnerabilities&lt;/strong&gt;: an unpatched server, a weak password, an employee who has never heard of phishing, a database left open to the internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk&lt;/strong&gt;: an active ransomware gang targeting your industry, plus your unpatched server, plus that server holding customer data. High likelihood, high impact, high risk.&lt;/p&gt;

&lt;p&gt;This also explains something that used to puzzle me. Why do companies leave some known vulnerabilities unfixed? Because risk is a business decision. If the vulnerability sits on an internal test machine with no valuable data, the risk is low and the fix can wait. Security teams do not fix everything. They fix what matters most first. That is called risk management, and it is the actual day job behind most security roles.&lt;/p&gt;

&lt;p&gt;📝 *&lt;em&gt;What I actually did: rating three risks in my own life&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Reading definitions is easy, so I forced myself to apply them. I listed three real risks in my own digital life and scored likelihood and impact from 1 to 5.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk 1&lt;/strong&gt;: password reuse. Threat: credential stuffing attacks using leaked password lists. Vulnerability: I reuse a password across several old accounts. Likelihood 4, impact 4. Score 16, my worst one. The fix costs nothing: a password manager and an hour of cleanup. That is this weekend's task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk 2&lt;/strong&gt;: phishing. Threat: phishing emails, which everyone receives. Vulnerability: I read email fast and on my phone, where sender addresses are hidden by default. Likelihood 3, impact 4, score 12. Fix: slow down on anything asking me to click or log in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk 3&lt;/strong&gt;: laptop theft. Threat: ordinary theft in cafes and transit. Vulnerability: my disk was not encrypted. Likelihood 2, impact 5, score 10. Fix: I turned on full disk encryption today. It took five minutes and one restart. If you do nothing else after reading this, check yours: BitLocker on Windows, FileVault on Mac.&lt;/p&gt;

&lt;p&gt;Try this exercise yourself. Three risks, two scores each, five minutes. It turns abstract vocabulary into a to do list, and it is exactly what professional risk assessments do at a much larger scale.&lt;/p&gt;

&lt;p&gt;🤔 &lt;strong&gt;What confused me today&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Is a hacker a threat or a threat actor? Strictly, the person or group is the threat actor, and the thing they might do is the threat. A ransomware gang is the actor, ransomware attack is the threat. People mix these constantly and the world keeps turning, but knowing the distinction helps when reading serious reports.&lt;/p&gt;

&lt;p&gt;Where do exploits fit? An exploit is the actual tool or technique that takes advantage of a vulnerability. Broken lock is the vulnerability, the crowbar technique is the exploit. We will meet real ones later in the series, safely and legally.&lt;/p&gt;

&lt;p&gt;⏭️ &lt;strong&gt;Next in this series&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Post 3 covers authentication versus authorization, the difference between proving who you are and what you are allowed to do. It is the concept behind every login screen you have ever seen, and the hands on part maps a real website login flow. Follow the series so you do not miss it.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>tutorial</category>
      <category>beginners</category>
      <category>security</category>
    </item>
    <item>
      <title>Smashing a hidden I/O bug: 100 disk reads per run for data that never changed</title>
      <dc:creator>sachin k</dc:creator>
      <pubDate>Mon, 17 Aug 2026 16:48:54 +0000</pubDate>
      <link>https://dev.to/sachinpk/smashing-a-hidden-io-bug-100-disk-reads-per-run-for-data-that-never-changed-2mb0</link>
      <guid>https://dev.to/sachinpk/smashing-a-hidden-io-bug-100-disk-reads-per-run-for-data-that-never-changed-2mb0</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Infrasity-Labs/awesome-tech-newsletter" rel="noopener noreferrer"&gt;awesome-tech-newsletter&lt;/a&gt; is an open-source project by Infrasity Labs that auto-discovers tech newsletters across the web. It runs a fleet of Python fetchers — for Substack, Hacker News, Medium, Product Hunt, Beehiiv, Hashnode, and more — that crawl each platform, classify what they find against a shared keyword config, and aggregate everything into a curated, categorized directory in the README.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;I picked this off the project's open issue tracker: &lt;a href="https://github.com/Infrasity-Labs/awesome-tech-newsletter/issues/41" rel="noopener noreferrer"&gt;issue #41&lt;/a&gt;, a performance bug in the Product Hunt fetcher.&lt;/p&gt;

&lt;p&gt;The fetcher pulls 100 recent posts from the Product Hunt GraphQL API, then loops over them to classify each post against a list of keyword queries. The problem was this line, sitting &lt;strong&gt;inside&lt;/strong&gt; the per-post loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;post_edge&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ...build text_corpus...
&lt;/span&gt;    &lt;span class="n"&gt;queries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_search_queries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;append_newsletter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# &amp;lt;-- every iteration
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cat&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That looks like a harmless list rebuild — until you read what &lt;code&gt;get_search_queries()&lt;/code&gt; actually does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_search_queries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;append_newsletter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;config_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;config.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="c1"&gt;# disk read
&lt;/span&gt;        &lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                          &lt;span class="c1"&gt;# JSON parse
&lt;/span&gt;    &lt;span class="c1"&gt;# ...build query list...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It &lt;strong&gt;opens and JSON-parses &lt;code&gt;config.json&lt;/code&gt; from disk on every single call&lt;/strong&gt;. So every fetch run was doing 100 file opens and 100 JSON parses for data that cannot change mid-loop.&lt;/p&gt;

&lt;p&gt;The smoking gun that this was a bug and not a design choice: every other fetcher in the repo (&lt;code&gt;hackernews.py&lt;/code&gt;, &lt;code&gt;substack.py&lt;/code&gt;, &lt;code&gt;medium.py&lt;/code&gt;, and six more) calls &lt;code&gt;get_search_queries()&lt;/code&gt; exactly once, outside its loop. Only &lt;code&gt;producthunt.py&lt;/code&gt; did it per-post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measured impact&lt;/strong&gt; (benchmarked against the repo's real &lt;code&gt;config.json&lt;/code&gt; — 9 categories, 86 generated queries — with 100 simulated posts):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric (per fetch run)&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;config.json&lt;/code&gt; file opens&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON parses&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Classification loop time&lt;/td&gt;
&lt;td&gt;5.14 ms&lt;/td&gt;
&lt;td&gt;0.14 ms (~36× faster)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Honest framing: this is a scheduled crawler, so no user was staring at a spinner. But the cost scales with post count × config size, and redundant loop-invariant I/O is exactly the kind of silent tax that compounds as a project grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;PR: &lt;a href="https://github.com/Infrasity-Labs/awesome-tech-newsletter/pull/43" rel="noopener noreferrer"&gt;https://github.com/Infrasity-Labs/awesome-tech-newsletter/pull/43&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The full change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;             posts = data.get('data', {}).get('posts', {}).get('edges', [])
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+            # Load search queries once: get_search_queries() reads and parses
+            # config.json from disk on every call, and its result never changes
+            # within a fetch run, so calling it per-post did 100 redundant
+            # file reads + JSON parses.
+            queries = get_search_queries(append_newsletter=False)
+            seen_urls = set()
+
&lt;/span&gt;             for post_edge in posts:
                 node = post_edge.get('node', {})
&lt;span class="p"&gt;@@
-                queries = get_search_queries(append_newsletter=False)
-                for query, cat in queries:
-                    if query in text_corpus:
+                for keyword, cat in queries:
+                    if keyword in text_corpus:
                         is_tech = True
                         category = cat
                         break
@@&lt;/span&gt;
&lt;span class="gd"&gt;-                    if not any(d['url'] == target_url for d in discovered):
&lt;/span&gt;&lt;span class="gi"&gt;+                    if target_url not in seen_urls:
+                        seen_urls.add(target_url)
&lt;/span&gt;                         logger.info("Discovered Product Hunt: %s", target_url)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;Three changes, in decreasing order of importance:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Hoisted the loop-invariant call.&lt;/strong&gt; &lt;code&gt;get_search_queries()&lt;/code&gt; now runs once per fetch, right after the API response is parsed. This turns 100 disk reads + JSON parses into 1. I deliberately did &lt;em&gt;not&lt;/em&gt; add caching inside &lt;code&gt;get_search_queries()&lt;/code&gt; itself — that would change behavior for every fetcher and risk serving stale config in long-lived processes. Fixing it at the single bad call site keeps the change surgical and matches the pattern the other nine fetchers already use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Fixed a variable-shadowing landmine.&lt;/strong&gt; The inner loop used &lt;code&gt;for query, cat in queries:&lt;/code&gt; — but &lt;code&gt;query&lt;/code&gt; was already the name of the GraphQL query string defined earlier in the same function. It happened to be harmless today because the GraphQL string isn't reused after the request, but it's the kind of shadowing that turns a future "add retry logic" PR into a mystery bug. Renamed the loop variable to &lt;code&gt;keyword&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Replaced an O(n²) dedup with a set.&lt;/strong&gt; The duplicate-URL check was &lt;code&gt;any(d['url'] == target_url for d in discovered)&lt;/code&gt; — a full list scan for every discovered post. A &lt;code&gt;seen_urls&lt;/code&gt; set makes it O(1) per lookup. Minor at 100 posts, but free to fix while I was in the function.&lt;/p&gt;

&lt;p&gt;Verification: the module compiles clean (&lt;code&gt;python3 -m py_compile&lt;/code&gt;), imports, and runs correctly through its no-token code path. The benchmark hooked &lt;code&gt;builtins.open&lt;/code&gt; to count file accesses, confirming the 100 → 1 drop, and used &lt;code&gt;timeit&lt;/code&gt; over 20 repeats for the timing numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transparency note:&lt;/strong&gt; I found, benchmarked, and fixed this bug working alongside Claude (Anthropic's AI assistant) — it ran the code analysis and benchmarking in a sandboxed environment while I directed the hunt, reviewed the changes, and submitted the PR.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
    <item>
      <title>Cybersecurity from Zero to Hero #1: The CIA Triad, or Why Your Bank App Keeps Logging You Out</title>
      <dc:creator>sachin k</dc:creator>
      <pubDate>Mon, 17 Aug 2026 14:14:50 +0000</pubDate>
      <link>https://dev.to/sachinpk/cybersecurity-from-zero-to-hero-1-the-cia-triad-or-why-your-bank-app-keeps-logging-you-out-225j</link>
      <guid>https://dev.to/sachinpk/cybersecurity-from-zero-to-hero-1-the-cia-triad-or-why-your-bank-app-keeps-logging-you-out-225j</guid>
      <description>&lt;p&gt;Welcome to post one of my new series, Cybersecurity from Zero to Hero. I am learning security from absolute scratch and writing about it as I go, so you get the explanations while the confusion is still fresh in my head. The plan is simple: concepts first, then networking, then Linux and web security, then real tools and practice platforms. No prior knowledge needed. If you are experienced, stick around anyway, because the comment sections are where you get to correct me and teach everyone else.&lt;/p&gt;

&lt;p&gt;One rule before we start, and it applies to this entire series. Every hands on exercise happens only on systems I own or on platforms built for legal practice like TryHackMe and OWASP Juice Shop. Learning security responsibly is part of learning security.&lt;/p&gt;

&lt;p&gt;Today we lay the foundation stone of all security thinking.&lt;/p&gt;

&lt;p&gt;The one framework everything else builds on&lt;/p&gt;

&lt;p&gt;Ask any security professional where to start and you will hear three letters: CIA. Not the agency. It stands for Confidentiality, Integrity and Availability, and it is the lens through which every security decision is made. Firewalls, passwords, backups, encryption, every tool you will ever meet in this field exists to protect one or more of these three properties.&lt;/p&gt;

&lt;p&gt;Here is what surprised me on day one of studying this. Security is not about stopping hackers. It is about protecting these three promises, and hackers are only one of many things that threaten them. A flood in a data center is a security event. So is an intern deleting the wrong folder.&lt;/p&gt;

&lt;p&gt;🔒 &lt;strong&gt;Confidentiality: only the right people can see it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Confidentiality means information is visible only to those authorized to see it. Your medical records, your salary, your private messages.&lt;/p&gt;

&lt;p&gt;Real world example. When your banking app logs you out after five minutes of inactivity, that is confidentiality at work. The bank assumes you might have walked away from your phone in a coffee shop. The aggressive timeout is not bad design. It is a deliberate tradeoff that sacrifices your convenience to protect your account from the stranger at the next table.&lt;/p&gt;

&lt;p&gt;Tools that protect it: encryption, passwords, multi factor authentication, access controls, and even the privacy screen on a laptop.&lt;/p&gt;

&lt;p&gt;The classic violation: a data breach where customer records leak to the public.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Integrity: the data has not been tampered with&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Integrity means information stays accurate and unaltered except by authorized changes. It is the promise that what you read is what was written.&lt;/p&gt;

&lt;p&gt;Real world example. When you transfer 100 dollars, integrity is the guarantee that it does not become 1000 dollars in transit, and that the recipient account number is not silently swapped. Banks obsess over this. An attacker who can change data is often more dangerous than one who can only read it.&lt;/p&gt;

&lt;p&gt;I also ran my first integrity check today, and you can too. When you download software, sites often publish a checksum next to the file. On Mac or Linux:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bash&lt;br&gt;
shasum -a 256 downloaded_file.zip&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
On Windows PowerShell:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;powershell&lt;br&gt;
Get-FileHash downloaded_file.zip&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Compare the output to the published value. If even one character differs, the file was corrupted or tampered with somewhere between their server and your machine. That comparison is integrity verification in its purest form, and later in this series we will build our own hashing tool in Python.&lt;/p&gt;

&lt;p&gt;Tools that protect it: hashing, checksums, digital signatures, version control, audit logs.&lt;/p&gt;

&lt;p&gt;The classic violation: an attacker altering a student database to change grades.&lt;/p&gt;

&lt;p&gt;⚡ &lt;strong&gt;Availability: the system works when you need it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Availability means authorized users can access the data and systems when they need them. Security that makes a system unusable has failed at its job.&lt;/p&gt;

&lt;p&gt;Real world example. When a Distributed Denial of Service attack floods a website with junk traffic until it collapses, nothing was stolen and nothing was altered. Confidentiality and integrity are intact. But the service is down, so it is absolutely a security incident. Backups, failover systems and disaster recovery plans all exist to protect this leg of the triangle.&lt;/p&gt;

&lt;p&gt;Tools that protect it: redundancy, backups, load balancing, DDoS protection, disaster recovery plans.&lt;/p&gt;

&lt;p&gt;The classic violation: ransomware locking a hospital out of its own patient records.&lt;/p&gt;

&lt;p&gt;🔺 The part that made it click for me: the triangle is a tension&lt;/p&gt;

&lt;p&gt;Here is the insight most beginner summaries skip. The three properties pull against each other, and real security work is about balancing them, not maximizing all three.&lt;/p&gt;

&lt;p&gt;Make a system maximally confidential with twelve authentication steps and you have destroyed availability. Nobody can get in, including the people who should. Make it maximally available with no logins at all and you have destroyed confidentiality. That annoying bank timeout is not a bug or lazy engineering. It is a chosen point on the triangle. Once I saw it this way, a dozen daily annoyances suddenly made sense as deliberate design decisions.&lt;/p&gt;

&lt;p&gt;A question for the experienced folks. When you review a system, do you consciously walk the triad, or has it become instinct? And what is your favorite example of a control that helps one leg while hurting another? Drop it in the comments and I will feature the best answers in a future post.&lt;/p&gt;

&lt;p&gt;🤔 What confused me today&lt;/p&gt;

&lt;p&gt;Where does authenticity fit? Some sources add authenticity and non repudiation as extra properties, and some textbooks talk about a hexad instead of a triad. For now I am parking that question. The triad is the standard mental model and the extensions can wait until the fundamentals are solid.&lt;/p&gt;

&lt;p&gt;Is a power outage really a security issue? My instinct said no, that is an IT problem. But under the triad, anything that threatens availability is in scope for security. The field is wider than the hacker movies suggest.&lt;/p&gt;

&lt;p&gt;⏭️ Next in this series&lt;/p&gt;

&lt;p&gt;Post 2 covers threats, vulnerabilities and risk, three words people use interchangeably that mean very different things. Once you can tell them apart, every security article you read becomes twice as clear. Follow the series so you do not miss it.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>security</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
