<?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: Bijan</title>
    <description>The latest articles on DEV Community by Bijan (@bijan53c).</description>
    <link>https://dev.to/bijan53c</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%2F3985731%2F41ac23dc-f551-4d29-a8a0-3fd79a35d893.jpeg</url>
      <title>DEV Community: Bijan</title>
      <link>https://dev.to/bijan53c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bijan53c"/>
    <language>en</language>
    <item>
      <title>Why I Chose Argon2id Over the \"Faster\" Alternatives</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Mon, 14 Sep 2026 17:00:00 +0000</pubDate>
      <link>https://dev.to/bijan53c/why-i-chose-argon2id-over-the-faster-alternatives-3dg1</link>
      <guid>https://dev.to/bijan53c/why-i-chose-argon2id-over-the-faster-alternatives-3dg1</guid>
      <description>&lt;p&gt;Argon2id is deliberately slow. That's not a limitation — it's the entire design goal. For key derivation, fast is a vulnerability, not a feature, and understanding why changed how I think about "performance" as a security property.&lt;/p&gt;

&lt;p&gt;This is part 3 of a series documenting what I'm learning building &lt;a href="https://github.com/bijan53c/CryptoGraphy" rel="noopener noreferrer"&gt;CryptoGraphy&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core trade-off: speed helps the attacker more than you
&lt;/h2&gt;

&lt;p&gt;Derive a key in one microsecond, and an attacker with the right hardware can try billions of password guesses per second against a stolen hash or ciphertext. Every guess is basically free for them. Make derivation slow and memory-hungry, and that same attack — still technically possible — becomes expensive enough to be impractical at scale.&lt;/p&gt;

&lt;p&gt;Here's the actual configuration in &lt;code&gt;crypto.py&lt;/code&gt;:&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;derive_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master_password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;hash_secret_raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;master_password&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="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="n"&gt;salt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;time_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;memory_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# 64 MiB
&lt;/span&gt;        &lt;span class="n"&gt;parallelism&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;hash_len&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&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;memory_cost=65536&lt;/code&gt; means each single derivation attempt requires 64MB of memory. &lt;code&gt;time_cost=3&lt;/code&gt; adds iteration on top of that. Individually these numbers look arbitrary. Together, they're the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why memory cost specifically matters
&lt;/h2&gt;

&lt;p&gt;Older key derivation functions like PBKDF2 are fast to attack on GPUs, because GPUs are built for exactly the kind of math PBKDF2 does: massively parallel, low-memory operations. A single high-end GPU can run an enormous number of parallel PBKDF2 guesses simultaneously, because each guess needs almost no memory.&lt;/p&gt;

&lt;p&gt;Argon2 breaks that advantage on purpose. By forcing every single guess to allocate and use real memory (64MB, in this config), it caps how many guesses can run in parallel — GPUs have far less memory available per core than a CPU does, so a GPU that could run thousands of parallel PBKDF2 attempts can only run a small fraction of that many parallel Argon2 attempts before running out of memory. This is called being "memory-hard," and it specifically neutralizes the GPU cracking rigs that are the actual hardware attackers use at scale, not a theoretical edge case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "id" specifically, not "i" or "d"
&lt;/h2&gt;

&lt;p&gt;Argon2 actually comes in three variants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Argon2d&lt;/strong&gt; — maximizes resistance to GPU cracking, but is vulnerable to certain side-channel timing attacks because memory access patterns depend on the secret input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Argon2i&lt;/strong&gt; — designed to resist those side-channel attacks by using data-independent memory access, at some cost to GPU resistance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Argon2id&lt;/strong&gt; — a hybrid that runs Argon2i-style access for part of the process and Argon2d-style for the rest, aiming to get meaningful resistance to both attack classes at once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That hybrid design is why Argon2id is the variant recommended by current guidance (including OWASP's password storage recommendations) over using Argon2i or Argon2d alone — it doesn't maximize either property individually, but it doesn't leave either one badly exposed either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tuning is a real trade-off, not a fixed answer
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;time_cost=3&lt;/code&gt;, &lt;code&gt;memory_cost=65536&lt;/code&gt;, &lt;code&gt;parallelism=4&lt;/code&gt; aren't universal constants — they're a balance between security and usability. Crank memory and time cost up, and you get stronger resistance to offline cracking, but every legitimate login or decryption also gets slower and heavier on the server or device doing the deriving. Set them too low, and you're back to a derivation cheap enough to attack at scale. There's no single correct number here — it depends on your threat model and what hardware your legitimate users are deriving keys on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Slow isn't a bug in a key derivation function — it's the mechanism. Argon2id spends real time and real memory on every single derivation specifically so that an attacker has to spend that same cost, multiplied by however many guesses they need to make. That's the trade a KDF is supposed to make, and it's a completely different goal from a general-purpose hash function like SHA-256, which is &lt;em&gt;meant&lt;/em&gt; to be fast.&lt;/p&gt;




&lt;p&gt;Next in this series: why encryption alone isn't enough, and what AES-GCM adds on top of confidentiality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you've tuned Argon2 or bcrypt cost parameters for a real production system, what trade-offs did you land on?&lt;/strong&gt; I'd like to hear how people are actually setting these in practice.&lt;/p&gt;

</description>
      <category>cryptography</category>
      <category>python</category>
      <category>programming</category>
      <category>security</category>
    </item>
    <item>
      <title>What a Confusing Line of Solidity Taught Me About Defaults and Security</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Sun, 06 Sep 2026 12:27:03 +0000</pubDate>
      <link>https://dev.to/bijan53c/what-a-confusing-line-of-solidity-taught-me-about-defaults-and-security-2ie7</link>
      <guid>https://dev.to/bijan53c/what-a-confusing-line-of-solidity-taught-me-about-defaults-and-security-2ie7</guid>
      <description>&lt;p&gt;Last night I spent an hour confused about something that should've taken two minutes.&lt;/p&gt;

&lt;p&gt;I was writing my first Solidity storage contract — a simple one, &lt;code&gt;SimpleStorage&lt;/code&gt;, that saves entries by ID. Coming from years of web app pentesting, SOC work, and red/purple team ops, I'm used to thinking about how systems get broken, not how they're built. So when I declared:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8h1ogfq4dt6l0htpibtc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8h1ogfq4dt6l0htpibtc.png" alt=" " width="800" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I expected I'd need to explicitly tell Solidity this was permanent — the same way I had to write &lt;code&gt;string memory _StringData&lt;/code&gt; for the temporary parameter in my function. I didn't. State variables like that mapping are storage by default, no keyword required. It took a tutorial, a web search, and finally asking AI directly before that clicked.&lt;/p&gt;

&lt;p&gt;Small thing. But it's the kind of small thing that matters more here than in most languages I've worked in — because in Solidity, a misunderstanding like that isn't just a bug. It's the difference between "works in testing" and "exploitable in production."&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: two things that look similar, aren't
&lt;/h2&gt;

&lt;p&gt;In most languages I've touched, "where does this variable live" is either irrelevant to the syntax or explicit everywhere. Solidity splits the difference in a way that isn't obvious until it bites you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inside a function, if you're working with a &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;struct&lt;/code&gt;, &lt;code&gt;array&lt;/code&gt;, or &lt;code&gt;mapping&lt;/code&gt;, you have to say whether it's &lt;code&gt;memory&lt;/code&gt; (temporary, wiped after the function call) or &lt;code&gt;storage&lt;/code&gt; (permanent, written to the blockchain).&lt;/li&gt;
&lt;li&gt;At the contract level — where you declare state variables — you don't say anything, because there's only one option. State variables are &lt;em&gt;always&lt;/em&gt; storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The inconsistency isn't a language flaw. It's that the "choice" only exists in one context. Outside a function, there's nothing to choose between — a state variable's whole purpose is to persist, so Solidity doesn't ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the default exists
&lt;/h2&gt;

&lt;p&gt;This maps directly to how the Ethereum Virtual Machine actually works. A smart contract has three places data can live:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Lifespan&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Where it's used&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;storage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Permanent, written to the blockchain&lt;/td&gt;
&lt;td&gt;Expensive (gas cost scales with writes)&lt;/td&gt;
&lt;td&gt;Contract state variables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Temporary, exists only during function execution&lt;/td&gt;
&lt;td&gt;Cheap&lt;/td&gt;
&lt;td&gt;Local variables inside functions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;calldata&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Temporary, read-only&lt;/td&gt;
&lt;td&gt;Cheapest&lt;/td&gt;
&lt;td&gt;Function input parameters (external calls)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;State variables exist specifically so a contract can remember something between transactions — a balance, an owner address, an entry in a mapping. There's no scenario where a state variable would need to be &lt;code&gt;memory&lt;/code&gt;, because a &lt;code&gt;memory&lt;/code&gt; variable disappears the moment the function returns. So Solidity doesn't make you spell out the obvious. Everything declared at the contract level, outside a function, is storage.&lt;/p&gt;

&lt;p&gt;Inside a function, though, the compiler can't guess your intent. You might want a &lt;code&gt;string&lt;/code&gt; that's just scratch space for a calculation — &lt;code&gt;memory&lt;/code&gt; — or you might be about to write it back into a state variable — &lt;code&gt;storage&lt;/code&gt;. That ambiguity is why the keyword is required there and only there.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it plays out in code
&lt;/h2&gt;

&lt;p&gt;Here's the shape of the confusion, side by side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// State variable — always storage, no keyword needed
mapping(uint256 =&amp;gt; string) public DataIdToData;

// Function parameter — location must be explicit
function addData(uint256 _id, string memory _StringData) public {
    DataIdToData[_id] = _StringData; // copied from memory into storage
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_StringData&lt;/code&gt; lives in memory for the duration of the function call. The moment you assign it into &lt;code&gt;DataIdToData[_id]&lt;/code&gt;, it's copied into permanent contract storage. Nothing about the syntax announces that copy is happening — you have to already know the mapping is &lt;code&gt;storage&lt;/code&gt; and the parameter is &lt;code&gt;memory&lt;/code&gt; to see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a security issue, not just a syntax quirk
&lt;/h2&gt;

&lt;p&gt;This is where the pentester instincts kicked back in. Getting &lt;code&gt;storage&lt;/code&gt; vs &lt;code&gt;memory&lt;/code&gt; wrong isn't cosmetic — it's a documented vulnerability class in Solidity, and it gets worse with structs and arrays.&lt;/p&gt;

&lt;p&gt;If you declare a local variable as &lt;code&gt;storage&lt;/code&gt; when you meant &lt;code&gt;memory&lt;/code&gt; (or the reverse), you're not creating a copy — you're creating a &lt;em&gt;reference&lt;/em&gt; to existing storage. Assign a &lt;code&gt;storage&lt;/code&gt; pointer to the wrong slot, or leave one uninitialized, and you can end up writing to state you never meant to touch. This general class of bug — storage layout confusion, particularly around delegatecall and library contracts — is widely cited in postmortems of real incidents, including the 2017 Parity multisig wallet freeze, where a storage layout issue tied to a library contract left funds permanently inaccessible. I'm citing that as a well-known industry example, not something from my own contract — but it's exactly the category of mistake that starts as "wait, which one is this again?"&lt;/p&gt;

&lt;p&gt;That's the pattern I keep running into three weeks in: things that would be a minor bug in a web app are a fund-draining or fund-freezing bug on-chain, because there's no "restart the server and it's fine" — the mistake is written to the chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm taking from this
&lt;/h2&gt;

&lt;p&gt;I've been studying Solidity for about three weeks now, doing it in the opposite order from how I originally learned security: build first, then go back and audit. It's blockchain, smart contracts, tokens, and a syntax that reads like a mix of Python, C++, and PHP — simpler than I expected, but with sharper edges than I'm used to.&lt;/p&gt;

&lt;p&gt;The practical habit I'm building from this: whenever Solidity &lt;em&gt;doesn't&lt;/em&gt; make me declare something, I now ask why not, instead of assuming it's not important. The implicit defaults are often exactly where the design decisions — and the risk — are concentrated.&lt;/p&gt;

&lt;p&gt;If you've made the same shift — from finding vulnerabilities to writing the code that creates them — what tripped you up first? What resources actually moved the needle for you when you were starting out?&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>security</category>
      <category>blockchain</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The One Line of Code That Stops Rainbow Table Attacks</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Wed, 02 Sep 2026 17:00:00 +0000</pubDate>
      <link>https://dev.to/bijan53c/the-one-line-of-code-that-stops-rainbow-table-attacks-1mca</link>
      <guid>https://dev.to/bijan53c/the-one-line-of-code-that-stops-rainbow-table-attacks-1mca</guid>
      <description>&lt;p&gt;Someone steals a database of password hashes. Instead of cracking each one individually, they check it against a rainbow table — a massive precomputed list mapping hashes to the passwords that produced them. If a stolen hash shows up in that table, they're done in milliseconds. No cracking required, just a lookup.&lt;/p&gt;

&lt;p&gt;Salting kills this attack entirely, and it's a small enough fix that it's worth understanding exactly why it works rather than just knowing "you're supposed to do it."&lt;/p&gt;

&lt;p&gt;This is part 2 of a series where I'm documenting what I'm learning building &lt;a href="https://github.com/bijan53c/CryptoGraphy" rel="noopener noreferrer"&gt;CryptoGraphy&lt;/a&gt;, a small Python project I'm using to actually understand applied cryptography instead of just calling library functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a salt actually does
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;main.py&lt;/code&gt;, before any key derivation happens, a fresh random salt is generated:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;salt&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="nf"&gt;urandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;derive_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That salt gets fed into Argon2id alongside the password:&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;derive_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master_password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;hash_secret_raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;master_password&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="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="n"&gt;salt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;time_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;memory_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;parallelism&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;hash_len&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&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 effect: two users with the identical password get two completely different derived keys, because their salts are different. Same input password, different salt, unrelated output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this specifically defeats rainbow tables
&lt;/h2&gt;

&lt;p&gt;A rainbow table only works because it's precomputed &lt;em&gt;once&lt;/em&gt; and reused against every hash in a stolen database. That only makes economic sense if the same password always produces the same hash — precompute "password123" → its hash one time, and that entry is useful forever, against any system, any user.&lt;/p&gt;

&lt;p&gt;Add a per-user salt, and that assumption breaks. Now "password123" with salt A produces a completely different output than "password123" with salt B. An attacker would need to precompute a separate rainbow table &lt;em&gt;for every possible salt value&lt;/em&gt;, which — with a 16-byte random salt — is computationally meaningless to attempt. You've turned one attack that scales across every victim into an attack that has to be redone, from scratch, per victim.&lt;/p&gt;

&lt;h2&gt;
  
  
  The salt isn't secret — and that's fine
&lt;/h2&gt;

&lt;p&gt;This is the detail that trips people up: the salt in &lt;code&gt;main.py&lt;/code&gt; isn't hidden anywhere. It's meant to sit right next to the ciphertext (or the stored hash) in plain sight, and that's by design.&lt;/p&gt;

&lt;p&gt;The salt's job was never to be a secret. Its job is to guarantee uniqueness — that no two derivations, even of the same password, ever produce the same result. Secrecy is the key's job. Uniqueness is the salt's job. They're different properties solving different problems, and a salt doesn't stop doing its job just because an attacker can see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is easy to skip and expensive to skip
&lt;/h2&gt;

&lt;p&gt;Skipping the salt costs you nothing while you're learning — the code still runs, the demo still works, the tests still pass. It's only expensive once real data is at stake, because "storing unsalted hashes" is one of those failures that's invisible until a breach happens, at which point it turns an already-bad leak into a catastrophic one: every user with a common password becomes crackable in bulk, instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;One &lt;code&gt;os.urandom(16)&lt;/code&gt; call, fed into key derivation, is the difference between "a leaked database is a slow, per-user cracking problem" and "a leaked database is a five-minute rainbow table lookup." It's a genuinely small piece of code carrying a genuinely large amount of the actual security guarantee.&lt;/p&gt;




&lt;p&gt;Next in this series: why I specifically chose Argon2id over faster alternatives like PBKDF2 — and why "slow" is the entire point of a key derivation function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have you dealt with a real system that turned out to be storing unsalted hashes?&lt;/strong&gt; Curious how that played out.&lt;/p&gt;

</description>
      <category>cryptography</category>
      <category>python</category>
      <category>security</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why You Can't Just Use a Password as an Encryption Key</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Tue, 01 Sep 2026 12:31:25 +0000</pubDate>
      <link>https://dev.to/bijan53c/why-you-cant-just-use-a-password-as-an-encryption-key-1nn6</link>
      <guid>https://dev.to/bijan53c/why-you-cant-just-use-a-password-as-an-encryption-key-1nn6</guid>
      <description>&lt;p&gt;I used to think encryption was simple: take a password, use it as the key, done. Then I built a small encryption tool myself, and realized that's not how any of this works.&lt;/p&gt;

&lt;p&gt;This is the first post in a series where I'm documenting what I'm actually learning while building &lt;a href="https://github.com/bijan53c/CryptoGraphy" rel="noopener noreferrer"&gt;CryptoGraphy&lt;/a&gt;, a small Python project I'm using to study applied cryptography properly instead of just calling library functions and hoping they're right. My background is in SOC analysis and pentesting — I'm used to &lt;em&gt;finding&lt;/em&gt; broken crypto, not building it. Writing this project is forcing me to understand the "why" behind the fixes I used to just recommend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive approach
&lt;/h2&gt;

&lt;p&gt;If you've never dug into how encryption actually works, this looks completely reasonable:&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="n"&gt;AES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&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;Pass in a password, get encrypted data back. It reads clean. It "works" in the sense that it runs without errors. And it's wrong in a way that's easy to miss if nobody ever shows you why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it breaks
&lt;/h2&gt;

&lt;p&gt;AES doesn't take a password. It takes a &lt;strong&gt;key&lt;/strong&gt;, and that key has to be an exact size — in my project, 256 bits (32 bytes).&lt;/p&gt;

&lt;p&gt;A password is neither of those things. It's variable-length, human-chosen, and (unless your users are unusually disciplined) low-entropy. If you pad or truncate a password to force it into 32 bytes, you haven't created a strong key — you've created a shortcut for an attacker. They don't need to break AES. They just need to guess the password, since the password &lt;em&gt;is&lt;/em&gt; the key in disguise.&lt;/p&gt;

&lt;p&gt;This matters because passwords and keys have completely different jobs. A password needs to be memorable to a human. A key needs to be unpredictable to a computer. Treating them as interchangeable collapses two different security properties into one weak one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: derive the key, don't reuse the password
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;crypto.py&lt;/code&gt;, the password never touches AES directly. It goes through a key derivation function first — specifically Argon2id:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;argon2.low_level&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hash_secret_raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt;

&lt;span class="n"&gt;SALT_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;
&lt;span class="n"&gt;KEY_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;derive_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master_password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;hash_secret_raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;master_password&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="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="n"&gt;salt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;time_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;memory_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;parallelism&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;hash_len&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;KEY_SIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&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;Password goes in. A properly-sized, unpredictable 32-byte key comes out. That key is what actually gets handed to AES:&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="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;derive_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part isn't just that the output is the right size — it's that Argon2id is deliberately expensive to compute. Guessing a password and running it through Argon2id to check if it produces the right key costs real time and memory, per guess. That cost is what turns "guess the password, get the key" from a fast attack into a slow, expensive one. (I go into exactly why that cost matters in the next post on Argon2id specifically.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The password and the key are two different things, with two different jobs, and conflating them is one of the most common ways homegrown crypto quietly fails. Most tutorials that show &lt;code&gt;AES.encrypt(password, data)&lt;/code&gt; aren't lying to you exactly — they're just skipping the step that actually matters.&lt;/p&gt;

&lt;p&gt;If you're building anything that touches encryption, the question to ask isn't "what password should I use as my key" — it's "what's deriving my key, and how expensive is it to attack that derivation."&lt;/p&gt;




&lt;p&gt;This is part of a public series where I'm documenting what I learn building out &lt;a href="https://github.com/bijan53c/CryptoGraphy" rel="noopener noreferrer"&gt;CryptoGraphy&lt;/a&gt; — next up: why salting stops rainbow table attacks completely, even against a leaked hash database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have you seen a real project use a password directly as a key?&lt;/strong&gt; I'd genuinely like to hear the story — it's more common than it should be.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>learning</category>
      <category>python</category>
      <category>security</category>
    </item>
    <item>
      <title>When Recon Fights Back: How a Giant's Infrastructure Made My Life Harder</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Sat, 18 Jul 2026 17:14:16 +0000</pubDate>
      <link>https://dev.to/bijan53c/when-recon-fights-back-how-a-giants-infrastructure-made-my-life-harder-3d91</link>
      <guid>https://dev.to/bijan53c/when-recon-fights-back-how-a-giants-infrastructure-made-my-life-harder-3d91</guid>
      <description>&lt;p&gt;I started working on a bug bounty program — a wide scope target, a giant in the gaming industry. I won't name them.&lt;/p&gt;

&lt;p&gt;Wide scope meant one thing to me: lots of untouched endpoints, lots of juicy stuff waiting to be found. I fired up my tools, kicked off the automation and waited.&lt;/p&gt;

&lt;p&gt;When I got back to the results file, I had plenty — subdomains, endpoints, the works. At first glance it looked great.&lt;/p&gt;

&lt;p&gt;Then I looked closer.&lt;/p&gt;

&lt;p&gt;URLs and domains I &lt;em&gt;knew&lt;/em&gt; didn't belong to this program were mixed throughout the output. Third party infrastructure was hosting their main domains in a cloud structure. Not ideal, but not a dealbreaker — ASN lookups, certificate transparency and DNS data should cut through the noise and help me map what actually belongs to them.&lt;/p&gt;

&lt;p&gt;Except they didn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  The ASN Problem
&lt;/h2&gt;

&lt;p&gt;Different domains that I was 100% certain belonged to the same company were returning completely different ASN data. No shared blocks, no common ground. Dead end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DNS Problem
&lt;/h2&gt;

&lt;p&gt;Same story. DNS data wasn't helping me connect the dots — it was just adding more noise to an already noisy output.&lt;/p&gt;

&lt;p&gt;I tried well known tools like AssetFinder. Same results. At this point I started questioning my own workflow, but the problem wasn't me — it was the infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Certificate Problem
&lt;/h2&gt;

&lt;p&gt;Here's where it got interesting. The company had 2-3 known domains that were clearly theirs, but each had been deployed in different years. Different deployment timelines meant different certificate issuers, different certificate data, no shared patterns to pivot from. Same story as the hosting — everything that should connect them, didn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  Coming Back with Fresh Eyes
&lt;/h2&gt;

&lt;p&gt;I stepped away and came back the next day.&lt;/p&gt;

&lt;p&gt;This time instead of chasing infrastructure signals, I went looking for something more tangible — something visible on the actual web pages of those 2-3 known domains.&lt;/p&gt;

&lt;p&gt;The copyright text was the first shared element I spotted. Seemed promising. I tried Google dorking it, built a Nuclei template to filter my noisy output for matches.&lt;/p&gt;

&lt;p&gt;Failure again.&lt;/p&gt;

&lt;p&gt;I dug into the source code and found out why — those elements were rendered by JavaScript. DOM elements. Not in the raw HTML. My template was looking for something that didn't exist at the source level.&lt;/p&gt;

&lt;p&gt;Same problem with the shared navigation menu sitting at the top of each site — same logo, same structure across domains — but again, pure DOM. Invisible to standard scraping.&lt;/p&gt;

&lt;p&gt;At this point I had two paths: write a Python or Node.js tool to render and parse the DOM, or find a smarter way with less grind. Writing a custom tool for a single program felt like overkill.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Smarter Move
&lt;/h2&gt;

&lt;p&gt;I went with three approaches that didn't require writing a single line of custom code:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Spider the shared navigation menu&lt;/strong&gt;&lt;br&gt;
That shared DOM menu linked to other assets belonging to the company. I used it as a starting point to manually map connected properties — essentially using their own navigation as a recon tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Track shared analytics scripts&lt;/strong&gt;&lt;br&gt;
Each of the known domains loaded 2-3 JavaScript analytics files from consistent addresses. Those JS file URLs were present in the raw HTML — unlike the DOM elements. I extracted them as samples, built a Nuclei template around them and ran it against my noisy output. This filtered the results down to assets genuinely connected to the target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Avoid generic string matching&lt;/strong&gt;&lt;br&gt;
I could have built templates around the company name or other common strings in the HTML, but that would have pulled in any third party site that mentioned them — news articles, partners, forums — adding even more noise rather than reducing it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Modern infrastructure — cloud hosting, third party CDNs, inconsistent deployment timelines — can make traditional recon signals completely unreliable. ASN, DNS and certificates are still valuable, but when a target's history is fragmented across different providers and different eras, you need to pivot to what's actually consistent: the product itself.&lt;/p&gt;

&lt;p&gt;Look at what the web pages share visually and functionally. Shared scripts, shared assets, shared navigation — these are the fingerprints that infrastructure can't hide.&lt;/p&gt;

&lt;p&gt;Have you hit a similar wall during recon? I'd love to hear how you handled it in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;#bugbounty #cybersecurity #infosec #recon #webdev&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>cybersecurity</category>
      <category>bugbounty</category>
    </item>
    <item>
      <title>A simple recon led me to a big rabbit hole from google dorks to find same company assets (web endpoints) to nuclei templates to separate python code. Day 3 in the rabbit hole :)</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Tue, 30 Jun 2026 13:50:44 +0000</pubDate>
      <link>https://dev.to/bijan53c/a-simple-recon-led-me-to-a-big-rabbit-hole-from-google-dorks-to-find-same-company-assets-web-37ab</link>
      <guid>https://dev.to/bijan53c/a-simple-recon-led-me-to-a-big-rabbit-hole-from-google-dorks-to-find-same-company-assets-web-37ab</guid>
      <description></description>
      <category>automation</category>
      <category>cybersecurity</category>
      <category>python</category>
      <category>security</category>
    </item>
    <item>
      <title>Email Verification Link Leading to Forced Account Takeover</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Sat, 27 Jun 2026 18:24:56 +0000</pubDate>
      <link>https://dev.to/bijan53c/email-verification-link-leading-to-forced-account-takeover-3dgp</link>
      <guid>https://dev.to/bijan53c/email-verification-link-leading-to-forced-account-takeover-3dgp</guid>
      <description>&lt;p&gt;What if clicking a completely legitimate verification link from a trusted domain could silently log you out and into an attacker's account — without you realizing it?&lt;br&gt;
That's exactly what this bug does.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Bug:
&lt;/h1&gt;

&lt;p&gt;A email verification flow was allowing an authenticated user's session to be hijacked by simply sending them a valid verification link belonging to a different account.&lt;br&gt;
No phishing. No fake domains. Just a real link from a real service.&lt;/p&gt;

&lt;h1&gt;
  
  
  How I found it
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;Register account A and verify it — stay logged in&lt;/li&gt;
&lt;li&gt;Register account B in a private window but don't verify it yet&lt;/li&gt;
&lt;li&gt;Copy account B's verification link&lt;/li&gt;
&lt;li&gt;Open it in the browser where account A is logged in&lt;/li&gt;
&lt;li&gt;Result: Account B gets verified, account A gets forcefully logged out, and the session switches to account B&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Real attack scenario
&lt;/h1&gt;

&lt;p&gt;The attacker registers an account with a similar looking email to the victim (s0mestring vs somestring) then sends the verification link to the victim — disguised as a support request:&lt;br&gt;
"Hi, I'm having trouble verifying my email, can you help?" + LINK&lt;br&gt;
Since the link is from a legitimate domain, the victim clicks it without suspicion.&lt;br&gt;
From that point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the victim was buying a subscription → attacker gets it&lt;/li&gt;
&lt;li&gt;If uploading files → they go to attacker's storage&lt;/li&gt;
&lt;li&gt;If granting access → attacker gets the permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even worse, the link uses a GET request, meaning it can be embedded as an image tag on any website, triggering the attack silently with zero clicks from the victim — a classic CSRF scenario.&lt;/p&gt;

&lt;h1&gt;
  
  
  Impact:
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Forced session hijacking&lt;/li&gt;
&lt;li&gt;Silent account switching via CSRF&lt;/li&gt;
&lt;li&gt;Subscription theft&lt;/li&gt;
&lt;li&gt;Access control manipulation&lt;/li&gt;
&lt;li&gt;Data theft&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Mitigation:
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Single-use tokens&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Once the verification link is clicked, invalidate the token immediately. It should never work a second time regardless of who clicks it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Short token expiry&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verification links should expire within 15-60 minutes of being issued. A link valid for days or forever is a much larger attack surface.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Session-aware verification&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;This is the core fix for this specific bug. When a verification link is clicked, the backend should check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is there already an active authenticated session in this browser?
Does that session belong to a different account than the one being verified?
If yes → reject the verification attempt and return an error, never switch sessions&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Don't touch existing sessions&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verification of account B should have zero effect on an existing session for account A. These should be completely independent server-side operations.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bind tokens to the registering context&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tie the verification token to the email address it was issued for. Clicking account B's link while logged in as account A should fail the context check immediately.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Takeaway:
&lt;/h1&gt;

&lt;p&gt;Authentication flows are some of the most overlooked attack surfaces. A verification link feels safe — it comes from a trusted domain, it's expected behavior. That's exactly what makes it dangerous when session management isn't handled correctly.&lt;br&gt;
Always ask: what happens if this link is opened by the wrong person?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cybersecurity</category>
      <category>bugbountyhunting</category>
      <category>security</category>
    </item>
    <item>
      <title>No user verification leading to subscription bypass and pre-register</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Fri, 19 Jun 2026 18:30:00 +0000</pubDate>
      <link>https://dev.to/bijan53c/no-user-verification-leading-to-subscription-bypass-and-pre-register-3dje</link>
      <guid>https://dev.to/bijan53c/no-user-verification-leading-to-subscription-bypass-and-pre-register-3dje</guid>
      <description>&lt;p&gt;For security reasons, we consider "Target app", as the target we practiced on, and the real name won't be disclosed in this post.&lt;/p&gt;

&lt;p&gt;The target app, was a niche music streaming platform, available in web and mobile PWA, meaning the structure is same but access is easier for cross platform.&lt;br&gt;
The app worked in this way :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You register using an account, 3rd party like google or via email&lt;/li&gt;
&lt;li&gt;After that you can use the app for free with a 3 day window (3 day trial)&lt;/li&gt;
&lt;li&gt;After the 3 day you gotta buy subscription to continue listening&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The flaw, existed in the first step, when you register using an email, no verification happens! You could enter any type of &lt;a href="mailto:string@something.com"&gt;string@something.com&lt;/a&gt;, a random password and start your free trial.&lt;br&gt;
So what happens is that first I use &lt;a href="mailto:string1@something.com"&gt;string1@something.com&lt;/a&gt;, for 3 days. When the time runs out, I use &lt;a href="mailto:string2@something.com"&gt;string2@something.com&lt;/a&gt; for another 3 days. And since the app's trial and actual subscription don't have any difference, and the 3 day time window is the only limitation, the user with such knowledge from the app doesn't need to buy any subscriptions while such flaw exists!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apply email verification step after user input, so they have to use the received link to verify their address&lt;/li&gt;
&lt;li&gt;Blacklist "temporary email" service's address or IPs, so users won't generate any email to register after their trial has expired.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This way, the registration process isn't too complex while keeps app from attackers avoiding a "For ever free" usage on the app.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>infosec</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Mobile view bug</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Wed, 17 Jun 2026 19:30:00 +0000</pubDate>
      <link>https://dev.to/bijan53c/mobile-view-bug-5bdd</link>
      <guid>https://dev.to/bijan53c/mobile-view-bug-5bdd</guid>
      <description>&lt;p&gt;There is a music streaming platform, which is supposed to only let registered users to play and download musics from it. On the desktop view of web app, as expected you can only see available playlists but no play button. So you have to register or log in, buy a subscription and then play or download a playlist.&lt;br&gt;
One of steps in testing a web app, is to check the mobile view of it. There are multiple ways to test that. The simplest is to make your browser window small, so according to viewable pixels the front end of the web app changes to the mobile mode. Some apps like this target have different views for different devices and viewable pixels. So we could now see the musics in the playlist, and &lt;code&gt;*click*&lt;/code&gt; we can play the musics in playlist.&lt;br&gt;
We still couldn't download it via the app, this one was hard coded for the users with subscriptions. But there was a way to save the musics which you can read here ==[[2026-05-03-WebApp-MobileView-SaveMusics-DRAFT]] &lt;em&gt;VIP&lt;/em&gt;== .&lt;/p&gt;

&lt;p&gt;As I said, we could turn to mobile view just by making our browser window smaller (vertical) to get a mobile view. But some apps identify mobile devices using different methods. So if that didn't work, you can change your user agent string to an exact IOS or Android browser user agent. If that is hard, use your browser's developer options / debugger to apply mobile view based on mobile device models (This can be different on different browsers, so I recommend to google it based on what you use). These 3 methods must work, but if you face a different scenario, I recommend you to check the HTTP headers too (These can lead to the backend but can be useful). &lt;/p&gt;

&lt;p&gt;I hope this comes in useful for ya.&lt;/p&gt;

</description>
      <category>web</category>
      <category>cybersecurity</category>
      <category>bugbounty</category>
      <category>security</category>
    </item>
    <item>
      <title>403 bypass scenarios</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Tue, 16 Jun 2026 19:35:00 +0000</pubDate>
      <link>https://dev.to/bijan53c/403-bypass-scenarios-2cg6</link>
      <guid>https://dev.to/bijan53c/403-bypass-scenarios-2cg6</guid>
      <description>&lt;h2&gt;
  
  
  A.Bad code :
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;I explain the security aspect of it and how to stop such vulnerabilities in separate post&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2.A Bad session management
&lt;/h3&gt;

&lt;p&gt;There are times that simple &lt;code&gt;Admin&lt;/code&gt; string in the &lt;code&gt;cookie&lt;/code&gt; or &lt;code&gt;session&lt;/code&gt; (or related custom headers), can cause you to authenticate to the app. For advance this simple string can be encoded to a commonly used format by the app (For example they use base64 a lot)&lt;br&gt;
This &lt;/p&gt;

&lt;h2&gt;
  
  
  B. WAF restriction
&lt;/h2&gt;

&lt;p&gt;There was an app that we had confirmed a reflected user input on it. We had to make sure they don't sanitize the input so we could send they payload for reflected XSS. We sent and we got 403. WTF? You could just remove the restricted part... why so serious? Got back on our recon data, we saw that they had a local made WAF using in front of web app which was not really well known. So we had to do a little bit of reverse engineering to understand how the waf works and what is bad in it's idea. After some tests we found out that the WAF as defined rules which work on the base64 format. They get the user input, turn base64 and check on the encoded data for malicious payloads, if OK it would decode the payload from base64 to URL and sent to web app. So first thing we tried was to encode the XSS to BASE64 and Viola. DONE! What we think is that the WAF encoded our base64 one more time, then checked it so the codes were different now (since they were encoded 2 times) and then decoded completely and sent to web app. And web app didn't have input sanitized and relied completely on the WAF as security so we got our payload running.&lt;/p&gt;

</description>
      <category>writeup</category>
      <category>cybersecurity</category>
      <category>web</category>
      <category>security</category>
    </item>
    <item>
      <title>Concept on bypassing 403 pages</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Mon, 15 Jun 2026 14:41:25 +0000</pubDate>
      <link>https://dev.to/bijan53c/concept-on-bypassing-403-pages-21p0</link>
      <guid>https://dev.to/bijan53c/concept-on-bypassing-403-pages-21p0</guid>
      <description>&lt;p&gt;On web application security, whether there's a pentest going on or hunting for vulnerabilities, there's a concept or technique for bypassing restrictions. In here we are going on details of bypassing 403 restrictions which usually lead to finding impactful vulnerabilities.&lt;/p&gt;

&lt;h1&gt;
  
  
  How the restriction works?
&lt;/h1&gt;

&lt;p&gt;For bypassing the restriction, we need to know how it works and what it does restrict; and it's obvious that different apps use different mechanisms so, let's explore some real world scenarios. &lt;/p&gt;

&lt;h2&gt;
  
  
  The overall concept
&lt;/h2&gt;

&lt;p&gt;For making a restriction, it has to be defined, so imagine it like a blacklist or whitelist mechanism which has to be bypassed. Now for bypassing them, you gotta understand what factors they are considering for restricting so, you would change focus on that item. Here we explore some types of restrictions:&lt;/p&gt;

&lt;h3&gt;
  
  
  IP restriction
&lt;/h3&gt;

&lt;p&gt;It's the simplest type, turn on your VPN. But remember that the IP can be set to whitelist some static IP's (e.g the admin's specific IP) which can make your job so much harder.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parameters restriction
&lt;/h3&gt;

&lt;p&gt;As you know, in each HTTP request we have some headers or parameters in the URL (For more simplicity I'm referring to both as &lt;em&gt;"Parameters"&lt;/em&gt;). Which they look something like these :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /somedirectory/?p=parameterdata
HOST: somesite.com
User-Agent: Mozilla...
Cookie: somestuff
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In above HTTP request, you can see parameters with &lt;code&gt;parameterdata&lt;/code&gt; value in URL, and &lt;em&gt;Headers&lt;/em&gt; as &lt;code&gt;HOST&lt;/code&gt;, &lt;code&gt;User-Agent&lt;/code&gt; and &lt;code&gt;Cookie&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Now imagine, the admin uses the &lt;em&gt;Admin Cookie&lt;/em&gt; as he has authenticated to visit &lt;code&gt;/OnlyAdmin/dash.html&lt;/code&gt;. So Anyone without the &lt;em&gt;Admin Cookie&lt;/em&gt; are not allowed to visit that page. So as our small example above, when you have a random cookie (not authenticated as admin) and visit the &lt;code&gt;/OnlyAdmin/dash.html&lt;/code&gt;, you face a 403 status code since you are not authenticated and allowed to visit that admin dashboard.&lt;/p&gt;

&lt;p&gt;So what is happening is that there is a restriction on that parameter/header. It's defined like ==If cookie = Admin --&amp;gt; allowed to visit &lt;code&gt;/OnlyAdmin/dash.html&lt;/code&gt; and if cookie !=(not equal) to Admin, return 403 and don't allow to visit.== &lt;br&gt;
So you are facing a whitelisting mechanism here. &lt;/p&gt;

&lt;p&gt;Before exploring more scenarios and bypassing them, let's have some blacklist examples too.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sites restricting certain user agents&lt;/li&gt;
&lt;li&gt;Restricting certain IP ranges (like people from certain country)&lt;/li&gt;
&lt;li&gt;Restricting users who send too much requests (rate limit)&lt;/li&gt;
&lt;li&gt;Bad user input (Like XSS, SQLi payloads)&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  So, how they are defined
&lt;/h2&gt;

&lt;p&gt;Usually they are set in 3 major ways which are :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some are defined by the &lt;em&gt;WAF (web application firewall)&lt;/em&gt; : For example blocking IP's or malicious parameters (e.g XSS,SQLi payloads)&lt;/li&gt;
&lt;li&gt;Some are put by the &lt;em&gt;reverse proxy&lt;/em&gt; : similar to waf but only processes what waf allowed to go in.&lt;/li&gt;
&lt;li&gt;Some are hardcoded by the developer (&lt;em&gt;source code&lt;/em&gt;): like the cookie scenario we explored&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;In next post I explain some bypass scenarios&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>security</category>
      <category>web</category>
      <category>writeup</category>
    </item>
    <item>
      <title>A bug that led to infinite likes on video sharing platform</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Mon, 15 Jun 2026 14:30:48 +0000</pubDate>
      <link>https://dev.to/bijan53c/a-bug-that-led-to-infinite-likes-on-video-sharing-platform-5faj</link>
      <guid>https://dev.to/bijan53c/a-bug-that-led-to-infinite-likes-on-video-sharing-platform-5faj</guid>
      <description>&lt;p&gt;There's a little fun bug I found on a video sharing website, which I can't tell the name of it, but I'm amazed that such bug exists on that platform cause it is more than 10 years for this platform to be online. &lt;br&gt;
With this bug, you can like a video infinite times without the need to log in. So you can become pretty popular with the likes you give to your own video (since more likes cause your video to get seen by more people), or idk, offer someone else amount of likes and get money instead?  Yeah this bug might not be that high impact at first glance or at all (Since I didn't investigate it further). But I'm gonna get into details here.&lt;/p&gt;

&lt;h1&gt;
  
  
  Normal behavior of web app (Getting a lead)
&lt;/h1&gt;

&lt;p&gt;The web app let you to like a video without authorization. That's already sus isn't it? But it could track you across browsers and sessions. E.g you use Firefox to like the video, you make a private tab to like again but... you see the video is already like by you. You switch to Chrome same happens. So you know what is going on, they are tracking users with the IP!&lt;/p&gt;

&lt;p&gt;In that moment I smiled, turned VPN on, checked the site with a fresh session, liked the video, changed IP again and ... What? Video is already liked?! First it might seem strange but, since there isn't an authentication for account to count likes (Exactly unlike Youtube), there must be a way.&lt;/p&gt;

&lt;p&gt;And the most interesting part is that this is not known as a bug from the developer (That's how they are working more than 10 years by the way). &lt;em&gt;It's a feature!&lt;/em&gt; This way people don't have to create and own accounts to like a video they see so it's easier for them to like the videos (Since the platform is not really popular and creating an account can take long so people would ignore and don't like and get on with their lives as they watched the video already and information was useful to them.) So the content creator would get likes easier.&lt;/p&gt;

&lt;h1&gt;
  
  
  The bug
&lt;/h1&gt;

&lt;p&gt;As I said, they track users from IP and setting session on browser side, since we changed IP on same session but the video was already liked. So how can we &lt;em&gt;Hack&lt;/em&gt; it for infinite likes?&lt;br&gt;
Easy, change both of the IP and session cookie/token/whatever that is set on browser side, and the app won't recognize you anymore.&lt;br&gt;
I changed IP while I opened a private tab, liked a random video, repeated the same loop again with a fresh IP and session, liked it again and so on. (I promise the content creator got really happy that day :) )&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating the process
&lt;/h2&gt;

&lt;p&gt;In theory, you just would need a proxy list or a script to change your system's IP while the like request is being made in fresh sessions.&lt;br&gt;
So 3 steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fresh session (probably selenium)&lt;/li&gt;
&lt;li&gt;Like the video &lt;/li&gt;
&lt;li&gt;Change IP (you can deploy scripts based on your solution (proxy,VPN,...))&lt;/li&gt;
&lt;li&gt;Repeat &lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  How to fix it?
&lt;/h1&gt;

&lt;p&gt;Apply user account authentication for liking videos. Authentication can be applied by an OTP to user phone number for faster and more effective authentication, or force user to authenticate before watching the video at first place.&lt;/p&gt;

</description>
      <category>security</category>
      <category>web</category>
      <category>cybersecurity</category>
    </item>
  </channel>
</rss>
