<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Hackita - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Hackita - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 19 Oct 2026 12:33:34 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/hackita-/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How ORMs Still Let SQL Injection Through (and How to Close the Gaps) ]]>
                </title>
                <description>
                    <![CDATA[ A lot of developers assume that once they're on an ORM, SQL injection stops being their problem. But it doesn't disappear. It just relocates. ORMs like Sequelize, Prisma, TypeORM, and Knex parameteriz ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-orms-still-let-sql-injection-through-and-how-to-close-the-gaps/</link>
                <guid isPermaLink="false">6abb5c6f814bf2c63668f9fc</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SQL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ node ]]>
                    </category>
                
                    <category>
                        <![CDATA[ orm ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Hackita ]]>
                </dc:creator>
                <pubDate>Tue, 29 Sep 2026 06:36:31 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/9e1f15e5-de00-4533-8349-70620154f7ef.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A lot of developers assume that once they're on an ORM, SQL injection stops being their problem. But it doesn't disappear. It just relocates.</p>
<p>ORMs like Sequelize, Prisma, TypeORM, and Knex parameterize the queries they build for you automatically. That part works.</p>
<p>The problem shows up in the parts of your app where you step outside that safe path: a raw query the ORM's query builder can't express cleanly, a dynamic sort column, or a single helper function reached for without a second thought.</p>
<p>Those are exactly the places <a href="https://hackita.it/articoli/hacker/">attackers go looking first</a>, and they're easy for developers to miss precisely because the rest of the codebase feels protected.</p>
<p>Four of those patterns are the focus here. Each one gets a working example that triggers the bug, a breakdown of why it's exploitable, and a rewritten version that closes it. I also include notes on exactly what moved and why.</p>
<p>You don't need a security background to follow along. You just need enough comfort with Node.js, SQL, and at least one ORM to read the code.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>The examples assume you're comfortable with:</p>
<ul>
<li><p>The basics of Node.js and Express</p>
</li>
<li><p>Writing basic SQL queries</p>
</li>
<li><p>Basic use of an ORM (all code examples use Sequelize, but the patterns apply to Prisma, TypeORM, Knex, and others too)</p>
</li>
<li><p>How an HTTP request/response cycle works</p>
</li>
</ul>
<p><strong>Note on code examples</strong>: Throughout this guide, <code>sequelize</code> is assumed to already be an initialized Sequelize instance, with <code>QueryTypes</code> and <code>Op</code> imported from <code>'sequelize'</code> alongside it, and <code>User</code>/<code>Product</code> are Sequelize models defined elsewhere in the app. Examples use Sequelize v6+ syntax (the current major version) since some of the APIs referenced here changed between major versions. You can adapt the syntax to the version and ORM you use.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-1-raw-query-escape-hatches">1. Raw Query Escape Hatches</a></p>
</li>
<li><p><a href="#heading-2-identifiers-that-cant-be-parameterized">2. Identifiers That Can't Be Parameterized</a></p>
</li>
<li><p><a href="#heading-3-second-order-injection-through-stored-data">3. Second-Order Injection Through Stored Data</a></p>
</li>
<li><p><a href="#heading-4-raw-sql-smuggled-into-normal-orm-calls">4. Raw SQL Smuggled Into Normal ORM Calls</a></p>
</li>
<li><p><a href="#heading-summary">Summary</a></p>
</li>
</ul>
<h2 id="heading-1-raw-query-escape-hatches">1. Raw Query Escape Hatches</h2>
<p>Every major ORM ships an "escape hatch" for queries the query builder can't express cleanly: <code>sequelize.query()</code>, Prisma's <code>$queryRawUnsafe</code>, or TypeORM's <code>query()</code>. They exist for good reasons, like complex joins, window functions, and vendor-specific SQL.</p>
<p>The trouble starts when developers treat that escape hatch like the rest of the ORM, and interpolate user input directly into the string it builds.</p>
<h3 id="heading-how-to-identify-this-vulnerability-in-your-code">How to Identify This Vulnerability in Your Code</h3>
<p>Here's a typical filtered report endpoint:</p>
<pre><code class="language-javascript">// Express.js - Vulnerable
app.get('/reports', async (req, res) =&gt; {
  const { region } = req.query;
  const results = await sequelize.query(
    `SELECT * FROM sales WHERE region = '${region}'`,
    { type: QueryTypes.SELECT }
  );
  res.json(results);
});
</code></pre>
<p>The query builder never sees this string. It's handed straight to the database driver exactly as written.</p>
<h4 id="heading-why-this-matters">Why This Matters</h4>
<p>A request like <code>?region=' OR '1'='1</code> turns the query into <code>SELECT * FROM sales WHERE region = '' OR '1'='1'</code>, which is always true. Every row in the table comes back, regardless of region.</p>
<p>Worse, because this code lives inside an ORM-based project, it often doesn't get the same scrutiny a raw <code>mysql.query()</code> call would in a non-ORM codebase. Reviewers assume the ORM already handled it.</p>
<h3 id="heading-how-to-fix-this-vulnerability">How to Fix This Vulnerability</h3>
<p>Pass values through the replacement/binding mechanism the raw-query API already provides, instead of building the string yourself:</p>
<pre><code class="language-javascript">// Express.js - Secure
app.get('/reports', async (req, res) =&gt; {
  const { region } = req.query;
  const results = await sequelize.query(
    'SELECT * FROM sales WHERE region = :region',
    {
      replacements: { region },
      type: QueryTypes.SELECT
    }
  );
  res.json(results);
});
</code></pre>
<p>The fix isn't avoiding raw queries entirely, because sometimes you genuinely need them. It's never building the SQL string by hand when a binding mechanism is sitting right there.</p>
<p><strong>Never concatenate user input into a raw query string. Use the replacement/binding API your raw-query method already provides.</strong></p>
<h2 id="heading-2-identifiers-that-cant-be-parameterized">2. Identifiers That Can't Be Parameterized</h2>
<p>Parameterized queries protect <em>values</em>. They don't protect <em>identifiers</em> like table names, column names, or <code>ORDER BY</code> direction. Those have to be part of the SQL string itself.</p>
<p>That's why a dynamic sort feature is one of the most common places injection sneaks back into otherwise well-written ORM code.</p>
<h3 id="heading-how-to-identify-this-vulnerability-in-your-code">How to Identify This Vulnerability in Your Code</h3>
<p>Here's a typical sortable list endpoint:</p>
<pre><code class="language-javascript">// Express.js - Vulnerable
app.get('/users', async (req, res) =&gt; {
  const { sortBy = 'created_at' } = req.query;
  const users = await sequelize.query(
    `SELECT * FROM users ORDER BY ${sortBy}`,
    { type: QueryTypes.SELECT }
  );
  res.json(users);
});
</code></pre>
<p><code>sortBy</code> goes straight from the query string into the <code>ORDER BY</code> clause, with nothing in between.</p>
<h4 id="heading-why-this-matters">Why This Matters</h4>
<p><code>sortBy</code> looks like a harmless UI convenience until someone sends <code>created_at; DROP TABLE users; --</code>. Whether that exact payload runs depends on your database driver: Postgres's <code>pg</code> driver executes stacked statements like this by default, while MySQL's <code>mysql2</code> blocks them unless <code>multipleStatements: true</code> is explicitly set.</p>
<p>Either way, the underlying problem is the same: you have arbitrary SQL sitting in a position that was only ever meant to hold a column name.</p>
<h3 id="heading-how-to-fix-this-vulnerability">How to Fix This Vulnerability</h3>
<p>Since identifiers can't be bound as parameters, the only safe option is an allowlist:</p>
<pre><code class="language-javascript">// Express.js - Secure
const ALLOWED_SORT_COLUMNS = ['created_at', 'name', 'email'];

app.get('/users', async (req, res) =&gt; {
  const { sortBy = 'created_at' } = req.query;
  const column = ALLOWED_SORT_COLUMNS.includes(sortBy) ? sortBy : 'created_at';

  const users = await sequelize.query(
    `SELECT * FROM users ORDER BY ${column}`,
    { type: QueryTypes.SELECT }
  );
  res.json(users);
});
</code></pre>
<p>Never pass user input through to an identifier position, even after "sanitizing" it first. Sanitization for identifiers is much easier to get wrong than for values.</p>
<p><strong>Identifiers can't be parameterized. If user input decides a column or table name, allowlist it, don't sanitize it.</strong></p>
<h2 id="heading-3-second-order-injection-through-stored-data">3. Second-Order Injection Through Stored Data</h2>
<p>This one catches teams off guard because the input <em>was</em> parameterized...the first time it was written to the database.</p>
<p>The injection happens later, when that already-stored value gets reused inside a different, unparameterized query.</p>
<h3 id="heading-how-to-identify-this-vulnerability-in-your-code">How to Identify This Vulnerability in Your Code</h3>
<p>Here's a registration flow, and elsewhere in the codebase, an admin search feature:</p>
<pre><code class="language-javascript">// Express.js - Vulnerable

// Step 1: user registration, correctly parameterized
app.post('/register', async (req, res) =&gt; {
  await User.create({ username: req.body.username });
  res.json({ success: true });
});

// Step 2: somewhere else in the codebase, an admin search feature
app.get('/admin/search', async (req, res) =&gt; {
  const user = await User.findByPk(req.params.id);
  const results = await sequelize.query(
    `SELECT * FROM audit_log WHERE actor = '${user.username}'`,
    { type: QueryTypes.SELECT }
  );
  res.json(results);
});
</code></pre>
<p>Step 1 is completely safe on its own. Step 2 is where things go wrong.</p>
<h4 id="heading-why-this-matters">Why This Matters</h4>
<p>A username like <code>admin' OR '1'='1</code> passes through step 1 without any issue. Sequelize's <code>create()</code> parameterizes it, so it's stored exactly as typed. It sits in the database looking completely normal.</p>
<p>The payload only fires when step 2 pulls that value back out and drops it straight into a raw query string. "It's already in our own database" is not the same thing as "it's safe". It's still attacker-controlled if it originated from user input anywhere upstream.</p>
<h3 id="heading-how-to-fix-this-vulnerability">How to Fix This Vulnerability</h3>
<p>Same fix as pattern #1. You bind the value instead of concatenating it:</p>
<pre><code class="language-javascript">// Express.js - Secure

// Step 1 (registration) is unchanged — it was already parameterized

app.get('/admin/search', async (req, res) =&gt; {
  const user = await User.findByPk(req.params.id);
  const results = await sequelize.query(
    'SELECT * FROM audit_log WHERE actor = :actor',
    {
      replacements: { actor: user.username },
      type: QueryTypes.SELECT
    }
  );
  res.json(results);
});
</code></pre>
<p>The bug here is harder to spot than pattern #1, because the tainted data crosses a database round-trip before it becomes dangerous.</p>
<p><strong>Data from your own database isn't automatically safe. If it originated from user input anywhere upstream, it still needs to be parameterized everywhere it's used.</strong></p>
<h2 id="heading-4-raw-sql-smuggled-into-normal-orm-calls">4. Raw SQL Smuggled Into Normal ORM Calls</h2>
<p>Pattern #1 covered the <em>obvious</em> raw-query escape hatch, a method you reach for on purpose when you need real SQL. This one is sneakier: injection hiding inside a call that looks completely ORM-managed. It's not raw SQL, right? It's just one helper function among many.</p>
<h3 id="heading-how-to-identify-this-vulnerability-in-your-code">How to Identify This Vulnerability in Your Code</h3>
<p>Here's a typical filtered product listing:</p>
<pre><code class="language-javascript">// Express.js - Vulnerable
app.get('/products', async (req, res) =&gt; {
  const { minPrice } = req.query;
  const products = await Product.findAll({
    where: sequelize.literal(`price &gt; ${minPrice}`)
  });
  res.json(products);
});
</code></pre>
<p><code>Product.findAll()</code> looks like a fully safe, parameterized ORM call...right up until <code>sequelize.literal()</code> shows up inside it.</p>
<h4 id="heading-why-this-matters">Why This Matters</h4>
<p><code>literal()</code> tells Sequelize "don't touch this, insert it into the SQL exactly as written." Anything interpolated into it is exactly as injectable as pattern #1, just wearing a normal-looking ORM method as a disguise.</p>
<p>Send <code>?minPrice=0 OR 1=1</code> and the <code>WHERE</code> clause becomes unconditionally true: every row comes back, price filter or not. No stacked statement needed this time. It's a single boolean expression, so it works the same regardless of database driver.</p>
<h3 id="heading-how-to-fix-this-vulnerability">How to Fix This Vulnerability</h3>
<p>Stop reaching for <code>literal()</code>. Sequelize's own operator API already covers this case:</p>
<pre><code class="language-javascript">// Express.js - Secure
app.get('/products', async (req, res) =&gt; {
  const minPrice = Number(req.query.minPrice);

  if (!Number.isFinite(minPrice)) {
    return res.status(400).json({ error: 'minPrice must be a number' });
  }

  const products = await Product.findAll({
    where: { price: { [Op.gt]: minPrice } }
  });
  res.json(products);
});
</code></pre>
<p><code>Op.gt</code>, <code>Op.between</code>, <code>Op.in</code>, and the rest of Sequelize's operator API already cover the vast majority of cases people reach for <code>literal()</code> for, and they parameterize correctly by default. Validating that <code>minPrice</code> is actually a number, before it ever reaches the query, closes the gap even if a future refactor reintroduces <code>literal()</code> somewhere else.</p>
<p><strong>Grep for</strong> <code>literal(</code><strong>,</strong> <code>fn(</code><strong>, and your ORM's other raw escape hatches everywhere they appear in the codebase, not just inside the queries that are clearly "raw."</strong></p>
<h2 id="heading-summary">Summary</h2>
<p>For a quick recap, here's how the four patterns line up:</p>
<table>
<thead>
<tr>
<th>Pattern</th>
<th>Root Cause</th>
<th>Core Fix</th>
</tr>
</thead>
<tbody><tr>
<td>Raw Query Escape Hatches</td>
<td>User input concatenated into a raw query string</td>
<td>Bind values via the replacement/parameter API</td>
</tr>
<tr>
<td>Identifier Injection</td>
<td>Column/table/sort input can't be parameterized</td>
<td>Allowlist allowed identifiers explicitly</td>
</tr>
<tr>
<td>Second-Order Injection</td>
<td>Stored data trusted because it was parameterized once</td>
<td>Parameterize every query, including ones using your own stored data</td>
</tr>
<tr>
<td>Literal Injection</td>
<td>Raw SQL smuggled into an otherwise ORM-managed call</td>
<td>Avoid <code>literal()</code>/<code>raw()</code>, use the ORM's operator API instead</td>
</tr>
</tbody></table>
<p>None of these four bugs takes a skilled attacker to find. Each one is just a value that never made it into the ORM's parameterization path.</p>
<p>What's worth making part of your workflow: parameterize values, allowlist identifiers, treat data pulled from your own database as no safer than a fresh request, and treat <code>literal()</code>/<code>raw()</code> calls as exactly as dangerous as a dedicated raw-query method (no matter how safe the surrounding code looks).</p>
<p>Fixing these gaps is only half the picture. Knowing how someone actually goes looking for them (and chains them together during a real assessment) is the half most developers never see.</p>
<p>If you're curious about that offensive perspective, this <a href="https://hackita.it/articoli/sql-injection-orm/">deep dive on SQL injection through ORMs</a> walks through exactly how these four patterns get exploited step by step in a live penetration test.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Fix Common Web Application Security Vulnerabilities in Node.js ]]>
                </title>
                <description>
                    <![CDATA[ Here's something that tends to surprise developers who are new to security: most web vulnerabilities aren't the result of sophisticated attacks. They come from code patterns that look completely reaso ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-fix-common-web-application-security-vulnerabilities-in-node-js/</link>
                <guid isPermaLink="false">6a581c65dba34eeb664be8e2</guid>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ owasp ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Hackita ]]>
                </dc:creator>
                <pubDate>Wed, 15 Jul 2026 23:48:53 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/b2ec81b6-d6eb-41f0-9fa5-7570914ef97d.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Here's something that tends to surprise developers who are new to security: most web vulnerabilities aren't the result of sophisticated attacks. They come from code patterns that look completely reasonable: trusting a value from the URL, applying a request body to a database update, or running two queries where one should've been enough.</p>
<p>This guide covers six of those patterns. For each one, you'll see a real code example that creates the vulnerability, an explanation of what makes it dangerous, and a corrected version with notes on exactly what changed and why.</p>
<p>No security background is required to follow along here. It'll just help to have some familiarity with Node.js and SQL.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>The examples assume you're comfortable with:</p>
<ul>
<li><p>Node.js and Express.js basics</p>
</li>
<li><p>SQL queries</p>
</li>
<li><p>How HTTP requests and responses work</p>
</li>
<li><p>Basic authentication concepts (sessions, tokens)</p>
</li>
</ul>
<p><strong>Note on code examples</strong>: Throughout this tutorial, <code>db.query()</code> is a fictional database helper that returns a single row object for <code>SELECT</code> queries (or <code>null</code> if not found), and a result object for <code>INSERT</code>/<code>UPDATE</code> queries. The <code>connection.query()</code> in the race conditions section uses the <a href="https://github.com/sidorares/node-mysql2">mysql2</a> promise API directly, where <code>query()</code> returns <code>[rows, fields]</code>. Adapt the syntax to the database driver you use.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-1-broken-access-control-and-idor">1. Broken Access Control and IDOR</a></p>
</li>
<li><p><a href="#heading-2-mass-assignment">2. Mass Assignment</a></p>
</li>
<li><p><a href="#heading-3-prototype-pollution">3. Prototype Pollution</a></p>
</li>
<li><p><a href="#heading-4-race-conditions">4. Race Conditions</a></p>
</li>
<li><p><a href="#heading-5-business-logic-flaws">5. Business Logic Flaws</a></p>
</li>
<li><p><a href="#heading-6-jwt-misconfiguration">6. JWT Misconfiguration</a></p>
</li>
<li><p><a href="#heading-summary">Summary</a></p>
</li>
</ul>
<h2 id="heading-1-broken-access-control-and-idor">1. Broken Access Control and IDOR</h2>
<p><a href="https://owasp.org/Top10/A01_2021-Broken_Access_Control/">Broken Access Control</a> has topped the OWASP Top 10 since 2021, and it's not hard to see why. The most common form is <strong>Insecure Direct Object Reference (IDOR)</strong>: the application exposes a database ID in a URL, a user changes the number, and suddenly they're looking at someone else's data.</p>
<p>The fix seems obvious in hindsight. But it keeps appearing in production code because authentication and authorization get conflated. Confirming that a user is logged in is not the same as confirming they're allowed to access a specific resource.</p>
<h3 id="heading-how-to-identify-this-vulnerability-in-your-code">How to Identify This Vulnerability in Your Code</h3>
<p>Here's a typical user profile endpoint:</p>
<pre><code class="language-javascript">// Express.js - Vulnerable
app.get('/api/users/:id/profile', authenticate, async (req, res) =&gt; {
  const userId = req.params.id;

  const user = await db.query(
    'SELECT id, name, email, address FROM users WHERE id = ?',
    [userId]
  );

  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }

  res.json(user);
});
</code></pre>
<p>The <code>authenticate</code> middleware confirms that the request includes a valid token. But it doesn't confirm whether the authenticated user is allowed to access the requested resource.</p>
<p>Any authenticated user can request <code>/api/users/1/profile</code>, <code>/api/users/2/profile</code>, and so on and retrieve other users' data.</p>
<h3 id="heading-why-this-matters">Why This Matters</h3>
<p>Authentication confirms <em>who</em> you are. Authorization confirms <em>what you're allowed to do</em>. The code above does the first and skips the second entirely.</p>
<p>With a sequential numeric ID, a curious user doesn't need any special tools. They can just change <code>1</code> to <code>2</code> in the URL. But the same problem exists with UUIDs or slugs if the ownership check is missing.</p>
<h3 id="heading-how-to-fix-this-vulnerability">How to Fix This Vulnerability</h3>
<p>Verify server-side that the authenticated user owns — or is explicitly authorized to access — the requested resource:</p>
<pre><code class="language-javascript">// Express.js - Secure
app.get('/api/users/:id/profile', authenticate, async (req, res) =&gt; {
  // Reject anything that isn't a string of digits — parseInt("12abc") would return 12
  if (!/^\d+$/.test(req.params.id)) {
    return res.status(400).json({ error: 'Invalid user ID' });
  }

  const requestedId = Number(req.params.id);

  if (requestedId &lt; 1) {
    return res.status(400).json({ error: 'Invalid user ID' });
  }

  // The authenticated user's ID is set by the authenticate middleware
  const authenticatedId = req.user.id;

  // Enforce ownership: users can only access their own profile
  if (requestedId !== authenticatedId) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  const user = await db.query(
    'SELECT id, name, email, address FROM users WHERE id = ?',
    [requestedId]
  );

  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }

  res.json(user);
});
</code></pre>
<p>For admin endpoints that legitimately need to access any user, enforce role-based authorization explicitly:</p>
<pre><code class="language-javascript">// Admin endpoint with explicit role check
app.get('/api/admin/users/:id', authenticate, requireRole('admin'), async (req, res) =&gt; {
  if (!/^\d+$/.test(req.params.id)) {
    return res.status(400).json({ error: 'Invalid user ID' });
  }

  const userId = Number(req.params.id);

  const user = await db.query(
    'SELECT id, name, email, role FROM users WHERE id = ?',
    [userId]
  );

  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }

  res.json(user);
});
</code></pre>
<p>Here's what changed and why:</p>
<ul>
<li><p><code>/^\d+$/.test(req.params.id)</code> rejects anything that isn't a pure string of digits. <code>parseInt("12abc", 10)</code> would silently return <code>12</code> and pass further checks. The regex prevents this.</p>
</li>
<li><p><code>Number(req.params.id)</code> converts the already-validated string to a number safely.</p>
</li>
<li><p>The comparison <code>requestedId !== authenticatedId</code> enforces ownership.</p>
</li>
<li><p>Admin functionality is a separate endpoint with its own authorization check.</p>
</li>
</ul>
<p><strong>Never infer authorization from a URL parameter. Derive it from the authenticated session.</strong></p>
<h2 id="heading-2-mass-assignment">2. Mass Assignment</h2>
<p>Mass assignment is one of those vulnerabilities that's almost invisible when you write it. You're just being efficient, right? Why iterate over fields manually when you can pass the whole object?</p>
<p>The problem is that your database table knows about fields your users were never supposed to touch.</p>
<h3 id="heading-how-to-identify-this-vulnerability-in-your-code">How to Identify This Vulnerability in Your Code</h3>
<p>Here's a user profile update endpoint:</p>
<pre><code class="language-javascript">// Express.js - Vulnerable
app.put('/api/users/me', authenticate, async (req, res) =&gt; {
  const userId = req.user.id;

  // req.body contains everything the client sends
  const updates = req.body;

  await db.query(
    'UPDATE users SET ? WHERE id = ?',
    [updates, userId]
  );

  res.json({ success: true });
});
</code></pre>
<p>The <code>users</code> table has these columns:</p>
<pre><code class="language-sql">CREATE TABLE users (
  id           INT PRIMARY KEY,
  name         VARCHAR(100),
  email        VARCHAR(100),
  bio          TEXT,
  role         ENUM('user', 'moderator', 'admin') DEFAULT 'user',
  credits      INT DEFAULT 0,
  is_banned    BOOLEAN DEFAULT false
);
</code></pre>
<p>The developer intended users to update <code>name</code>, <code>email</code>, and <code>bio</code>. But <code>role</code>, <code>credits</code>, and <code>is_banned</code> are also in the table — and the query updates whatever fields the client sends.</p>
<h3 id="heading-why-this-matters">Why This Matters</h3>
<p>That request body goes straight into the SQL query. The <code>users</code> table also has <code>role</code>, <code>credits</code>, and <code>is_banned</code> — and the query doesn't know or care which fields the developer "intended" to expose.</p>
<p>A user who sends this:</p>
<pre><code class="language-json">{
  "name": "Alice",
  "role": "admin",
  "credits": 100000,
  "is_banned": false
}
</code></pre>
<p>Has just promoted themselves to admin, cleared their ban, and given themselves a hundred thousand credits.</p>
<h3 id="heading-how-to-fix-this-vulnerability">How to Fix This Vulnerability</h3>
<p>Build the update object yourself, field by field, using an explicit allowlist:</p>
<pre><code class="language-javascript">// Express.js - Secure
app.put('/api/users/me', authenticate, async (req, res) =&gt; {
  const userId = req.user.id;

  // Only these fields may be updated by the user
  const ALLOWED_FIELDS = ['name', 'email', 'bio'];
  const updates = {};

  for (const field of ALLOWED_FIELDS) {
    if (req.body[field] !== undefined) {
      updates[field] = req.body[field];
    }
  }

  if (Object.keys(updates).length === 0) {
    return res.status(400).json({ error: 'No valid fields provided' });
  }

  // Validate individual fields
  // isValidEmail is a simple helper: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
  if (updates.email &amp;&amp; !isValidEmail(updates.email)) {
    return res.status(400).json({ error: 'Invalid email format' });
  }

  if (updates.name &amp;&amp; (typeof updates.name !== 'string' || updates.name.length &gt; 100)) {
    return res.status(400).json({ error: 'Name must be a string of 100 characters or fewer' });
  }

  await db.query(
    'UPDATE users SET ? WHERE id = ?',
    [updates, userId]
  );

  res.json({ success: true });
});
</code></pre>
<p>For admin operations that legitimately update sensitive fields, use a separate endpoint with its own authorization:</p>
<pre><code class="language-javascript">// Admin-only endpoint for changing user roles
app.put('/api/admin/users/:id/role', authenticate, requireRole('admin'), async (req, res) =&gt; {
  if (!/^\d+$/.test(req.params.id)) {
    return res.status(400).json({ error: 'Invalid user ID' });
  }

  const userId = Number(req.params.id);
  const { role } = req.body;

  const VALID_ROLES = ['user', 'moderator', 'admin'];

  if (!VALID_ROLES.includes(role)) {
    return res.status(400).json({ error: 'Invalid role' });
  }

  await db.query(
    'UPDATE users SET role = ? WHERE id = ?',
    [role, userId]
  );

  res.json({ success: true });
});
</code></pre>
<p><strong>Don't spread</strong> <code>req.body</code> <strong>into a database query. Build the update object field by field.</strong></p>
<h2 id="heading-3-prototype-pollution">3. Prototype Pollution</h2>
<p>Prototype pollution occurs when untrusted data is merged into an object recursively, allowing an attacker to inject properties into <code>Object.prototype</code> (the base object that every plain JavaScript object inherits from).</p>
<p>The OWASP Top 10 covers this under <a href="https://owasp.org/Top10/A08_2021-Software_and_Data_Integrity_Failures/">Software and Data Integrity Failures (A08:2021)</a>.</p>
<h3 id="heading-how-to-identify-this-vulnerability-in-your-code">How to Identify This Vulnerability in Your Code</h3>
<p>A configuration merge utility that processes user-supplied settings:</p>
<pre><code class="language-javascript">// Vulnerable recursive merge function
function mergeConfig(target, source) {
  for (const key of Object.keys(source)) {
    if (typeof source[key] === 'object' &amp;&amp; source[key] !== null) {
      if (!target[key]) target[key] = {};
      mergeConfig(target[key], source[key]); // recursive call
    } else {
      target[key] = source[key]; // triggers __proto__ setter via bracket notation
    }
  }
}

app.post('/api/settings', authenticate, (req, res) =&gt; {
  const userSettings = {};
  mergeConfig(userSettings, req.body); // merge untrusted input
  applySettings(userSettings);
  res.json({ success: true });
});
</code></pre>
<h3 id="heading-why-this-matters">Why This Matters</h3>
<p>An attacker sends this request body:</p>
<pre><code class="language-json">{
  "__proto__": {
    "isAdmin": true
  }
}
</code></pre>
<p>The recursive <code>mergeConfig</code> function reaches the <code>__proto__</code> key and executes <code>target['__proto__']['isAdmin'] = true</code>. Because <code>__proto__</code> is JavaScript's prototype accessor, this writes directly to <code>Object.prototype</code>. After the merge:</p>
<pre><code class="language-javascript">const anyObject = {};
console.log(anyObject.isAdmin); // true — inherited from Object.prototype
</code></pre>
<p>Every plain object in the running application now inherits <code>isAdmin: true</code>. If any authorization check looks like this:</p>
<pre><code class="language-javascript">if (user.isAdmin) { /* grant admin access */ }
</code></pre>
<p>That check now passes for every user, regardless of their actual role.</p>
<h3 id="heading-how-to-fix-this-vulnerability">How to Fix This Vulnerability</h3>
<p>Store user state server-side and validate each field individually. Never recursively merge untrusted input:</p>
<pre><code class="language-javascript">// Express.js - Secure
app.post('/api/settings', authenticate, async (req, res) =&gt; {
  // Load current settings from the database — never from the client
  const current = await db.query(
    'SELECT theme, language, notifications FROM user_settings WHERE user_id = ?',
    [req.user.id]
  );

  // Validate each field against an explicit allowlist
  const safeSettings = {
    theme: validateEnum(req.body.theme, ['light', 'dark'], current.theme),
    language: validateEnum(req.body.language, ['en', 'es', 'fr', 'de'], current.language),
    notifications: typeof req.body.notifications === 'boolean'
      ? req.body.notifications
      : current.notifications
  };

  await db.query(
    'UPDATE user_settings SET ? WHERE user_id = ?',
    [safeSettings, req.user.id]
  );

  res.json({ success: true });
});

function validateEnum(value, allowed, defaultValue) {
  return allowed.includes(value) ? value : defaultValue;
}
</code></pre>
<p>If you need a merge utility, use <code>Object.create(null)</code> as the base — it has no prototype, so <code>__proto__</code> can't be polluted — and allowlist keys explicitly:</p>
<pre><code class="language-javascript">// Safe merge: base object with no prototype
function safeMerge(allowedKeys, source) {
  const result = Object.create(null); // no prototype = no pollution possible

  for (const key of allowedKeys) {
    if (key in source &amp;&amp; typeof source[key] !== 'object') {
      result[key] = source[key];
    }
  }

  return result;
}
</code></pre>
<p><strong>Rules</strong>:</p>
<ul>
<li><p>Never recursively merge untrusted input into a plain object.</p>
</li>
<li><p>Store application state server-side. Don't trust clients to carry it.</p>
</li>
<li><p>Use <code>Object.create(null)</code> for data containers that will hold untrusted keys.</p>
</li>
<li><p>Validate each field by type and allowed values before using it.</p>
</li>
</ul>
<h2 id="heading-4-race-conditions">4. Race Conditions</h2>
<p>Race conditions are tricky because the code is perfectly correct. It's the timing that breaks it. Two requests arrive at almost the same moment, both check the same condition, both see a valid result, and both proceed. The result is something that should only happen once, happening twice.</p>
<p>This is called a <strong>Time-of-Check to Time-of-Use (TOCTOU)</strong> problem: the state you checked is no longer the state you're acting on.</p>
<h3 id="heading-how-to-identify-this-pattern-in-your-code">How to Identify This Pattern in Your Code</h3>
<p>A single-use coupon redemption endpoint:</p>
<pre><code class="language-javascript">// Express.js - Vulnerable
app.post('/api/redeem-coupon', authenticate, async (req, res) =&gt; {
  const { couponCode } = req.body;
  const userId = req.user.id;

  // Step 1: Check if the coupon is still valid
  const coupon = await db.query(
    'SELECT id, discount_amount, used FROM coupons WHERE code = ? AND used = false',
    [couponCode]
  );

  if (!coupon) {
    return res.status(400).json({ error: 'Invalid or already used coupon' });
  }

  // Time gap: another request can pass Step 1 here before Step 3 runs

  // Step 2: Apply the discount
  await applyDiscountToOrder(userId, coupon.discount_amount);

  // Step 3: Mark coupon as used
  await db.query(
    'UPDATE coupons SET used = true, used_by = ? WHERE code = ?',
    [userId, couponCode]
  );

  res.json({ success: true });
});
</code></pre>
<h3 id="heading-why-this-matters">Why This Matters</h3>
<p>Two requests arrive with the same coupon code at nearly the same time. Both hit Step 1 before either reaches Step 3. Both read <code>used = false</code>. Both apply the discount. One coupon, used twice.</p>
<p>The same window exists anywhere you read-then-write: balance checks before deductions, inventory checks before reservations, vote checks before incrementing. Any of them can be exploited the same way.</p>
<h3 id="heading-how-to-fix-this-vulnerability">How to Fix This Vulnerability</h3>
<p>Replace the check-then-act pattern with an atomic operation. An atomic database update guarantees that the condition check and the write happen as a single indivisible unit:</p>
<pre><code class="language-javascript">// Express.js - Secure
app.post('/api/redeem-coupon', authenticate, async (req, res) =&gt; {
  const { couponCode } = req.body;
  const userId = req.user.id;

  const connection = await db.getConnection();

  try {
    await connection.beginTransaction();

    // Atomic: only one request can update a row where used = false.
    // The database row lock ensures only one request succeeds.
    const [result] = await connection.query(
      `UPDATE coupons
       SET used = true, used_by = ?, used_at = NOW()
       WHERE code = ? AND used = false`,
      [userId, couponCode]
    );

    if (result.affectedRows === 0) {
      await connection.rollback();
      return res.status(400).json({ error: 'Invalid or already used coupon' });
    }

    const [couponRows] = await connection.query(
      'SELECT discount_amount FROM coupons WHERE code = ?',
      [couponCode]
    );
    const coupon = couponRows[0];

    await applyDiscountToOrder(userId, coupon.discount_amount, connection);

    await connection.commit();

    res.json({ success: true, discount: coupon.discount_amount });

  } catch (error) {
    await connection.rollback();
    console.error('Coupon redemption failed:', error);
    res.status(500).json({ error: 'Could not process the coupon' });
  } finally {
    connection.release();
  }
});
</code></pre>
<p>The key change is <code>UPDATE ... WHERE code = ? AND used = false</code>. The database acquires a row lock during the update. Only one concurrent request can succeed. The second request finds <code>affectedRows = 0</code> and returns an error — correctly.</p>
<p><strong>Any time you read a value to make a decision before writing — that's a potential race condition. Make the check and the write atomic.</strong></p>
<h2 id="heading-5-business-logic-flaws">5. Business Logic Flaws</h2>
<p>Business logic flaws are the hardest category to catch. Automated scanners won't find them, and code review might miss them too, because the code works exactly as written. The problem is in what the code was designed to do, not how it does it.</p>
<p>The most common form: trusting the client to send sensible numeric values.</p>
<h3 id="heading-how-to-identify-this-vulnerability-in-your-code">How to Identify This Vulnerability in Your Code</h3>
<p>An e-commerce checkout endpoint:</p>
<pre><code class="language-javascript">// Express.js - Vulnerable
app.post('/api/checkout', authenticate, async (req, res) =&gt; {
  const { items } = req.body;

  let total = 0;
  const processedItems = [];

  for (const item of items) {
    const product = await db.query(
      'SELECT id, price FROM products WHERE id = ?',
      [item.productId]
    );

    if (!product) {
      return res.status(400).json({ error: `Product not found: ${item.productId}` });
    }

    // Trust the quantity value from the client
    const itemTotal = product.price * item.quantity;
    total += itemTotal;

    processedItems.push({ productId: product.id, quantity: item.quantity, price: product.price });
  }

  if (total &gt; 100) {
    total = total * 0.9; // 10% discount
  }

  await createOrder(req.user.id, processedItems, total);
  res.json({ success: true, total });
});
</code></pre>
<h3 id="heading-why-this-matters">Why This Matters</h3>
<p><strong>Problem 1 — Negative quantity</strong>: The code multiplies the server's price by the client's quantity without checking that the quantity is positive. A user sends <code>"quantity": -5</code> on an expensive item. Its contribution to the total becomes negative, reducing the overall total.</p>
<p><strong>Problem 2 — Floating point arithmetic</strong>: <code>0.1 + 0.2</code> in JavaScript is <code>0.30000000000000004</code>. Without rounding, financial calculations accumulate errors over time.</p>
<p><strong>Problem 3 — Discount manipulation</strong>: If a separate endpoint allows modifying an order after checkout without recalculating the total, a user could add items to earn the discount, then remove items while keeping the discounted price.</p>
<h3 id="heading-how-to-fix-this-vulnerability">How to Fix This Vulnerability</h3>
<p>Validate every numeric input and recalculate all totals server-side. Use integer arithmetic (cents) for money to avoid floating point errors:</p>
<pre><code class="language-javascript">// Express.js - Secure
app.post('/api/checkout', authenticate, async (req, res) =&gt; {
  const { items } = req.body;

  if (!Array.isArray(items) || items.length === 0) {
    return res.status(400).json({ error: 'Cart must contain at least one item' });
  }

  if (items.length &gt; 50) {
    return res.status(400).json({ error: 'Cart cannot contain more than 50 items' });
  }

  let totalCents = 0; // Integer arithmetic avoids floating point errors
  const processedItems = [];

  for (const item of items) {
    if (!/^\d+$/.test(String(item.productId))) {
      return res.status(400).json({ error: `Invalid product ID: ${item.productId}` });
    }

    const productId = Number(item.productId);

    // Validate quantity: must be a string of digits only, between 1 and 10
    // parseInt("3abc", 10) returns 3 — we use regex to prevent this
    if (!/^\d+$/.test(String(item.quantity))) {
      return res.status(400).json({
        error: `Invalid quantity for product ${productId}`
      });
    }

    const quantity = Number(item.quantity);

    if (quantity &lt; 1 || quantity &gt; 10) {
      return res.status(400).json({
        error: `Quantity for product ${productId} must be between 1 and 10`
      });
    }

    const product = await db.query(
      'SELECT id, name, price_cents, stock FROM products WHERE id = ? AND active = true',
      [productId]
    );

    if (!product) {
      return res.status(400).json({ error: `Product not found: ${productId}` });
    }

    if (product.stock &lt; quantity) {
      return res.status(400).json({
        error: `Insufficient stock for "${product.name}"`
      });
    }

    // Use the server's price — never trust a price from the client
    totalCents += product.price_cents * quantity;

    processedItems.push({
      productId: product.id,
      name: product.name,
      quantity,
      unitPriceCents: product.price_cents
    });
  }

  // Integer arithmetic for the discount calculation
  const discountMultiplier = totalCents &gt; 10000 ? 90 : 100; // 10000 cents = $100
  const finalTotalCents = Math.round(totalCents * discountMultiplier / 100);

  await createOrder(req.user.id, processedItems, finalTotalCents);

  res.json({
    success: true,
    total: (finalTotalCents / 100).toFixed(2),
    currency: 'USD'
  });
});
</code></pre>
<p>What changed and why:</p>
<ul>
<li><p>Integer (cents) arithmetic eliminates floating point errors. <code>10000 + 3000</code> in integer cents is always exact.</p>
</li>
<li><p><code>quantity &lt; 1 || quantity &gt; 10</code> prevents negative quantities and unreasonably large orders.</p>
</li>
<li><p><code>items.length &gt; 50</code> prevents oversized requests.</p>
</li>
<li><p><code>product.stock &lt; quantity</code> ensures the order doesn't exceed available inventory.</p>
</li>
<li><p>All totals, discounts, and final prices are calculated server-side from server-side prices.</p>
</li>
</ul>
<p><strong>Define the valid range for every numeric input. Recalculate every total server-side. The client isn't a trusted source for prices or quantities.</strong></p>
<h2 id="heading-6-jwt-misconfiguration">6. JWT Misconfiguration</h2>
<p>JWTs are everywhere in Node.js APIs, and the <code>jsonwebtoken</code> library makes them easy to use. That's both good and bad: they're easy to use correctly, but they're also easy to use in ways that look fine until they're not.</p>
<p>The OWASP Top 10 classifies authentication failures under <a href="https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/">Identification and Authentication Failures (A07:2021)</a>. Three misconfigurations show up repeatedly.</p>
<h3 id="heading-how-to-identify-this-vulnerability-in-your-code">How to Identify This Vulnerability in Your Code</h3>
<p><strong>Mistake 1 — Algorithm not specified in</strong> <code>verify()</code>:</p>
<pre><code class="language-javascript">// Vulnerable
const decoded = jwt.verify(token, process.env.JWT_SECRET);
</code></pre>
<p>Without an <code>algorithms</code> option, some JWT implementations can be tricked into accepting tokens that declare <code>"alg": "none"</code> in their header — meaning no signature is required at all.</p>
<p><strong>Mistake 2 — Weak or hardcoded secrets</strong>:</p>
<pre><code class="language-javascript">// Vulnerable
const token = jwt.sign({ userId: user.id }, 'secret');
</code></pre>
<p>A short or predictable HS256 secret can be brute-forced offline if an attacker obtains a valid token.</p>
<p><strong>Mistake 3 — Sensitive data in the payload</strong>:</p>
<pre><code class="language-javascript">// Vulnerable
const token = jwt.sign({
  userId: user.id,
  passwordHash: user.passwordHash, // never do this
  role: user.role
}, secret);
</code></pre>
<p>JWT payloads are base64-encoded, not encrypted. Anyone who holds the token can decode the payload and read its contents.</p>
<h3 id="heading-how-to-fix-these-issues">How to Fix These Issues</h3>
<pre><code class="language-javascript">// Secure JWT implementation
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// Generate a strong secret once and store it as an environment variable:
// node -e "console.log(crypto.randomBytes(64).toString('hex'))"
const JWT_SECRET = process.env.JWT_SECRET;

if (!JWT_SECRET || Buffer.from(JWT_SECRET, 'hex').length &lt; 32) {
  throw new Error('JWT_SECRET must be at least 32 random bytes');
}

function signToken(userId) {
  return jwt.sign(
    { sub: userId },         // 'sub' is the standard claim for the user identifier
    JWT_SECRET,
    {
      algorithm: 'HS256',    // always declare the algorithm explicitly
      expiresIn: '15m',      // short-lived tokens reduce the window if a token is stolen
      issuer: 'your-app-name',
      audience: 'your-app-name'
    }
  );
}

function verifyToken(token) {
  return jwt.verify(token, JWT_SECRET, {
    algorithms: ['HS256'],   // whitelist only the expected algorithm
    issuer: 'your-app-name',
    audience: 'your-app-name'
  });
}

function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const decoded = verifyToken(token);
    req.user = { id: decoded.sub };
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}
</code></pre>
<p><strong>A note on token revocation</strong>: JWTs are stateless, meaning your server keeps no record of them. This means you can't immediately invalidate a token on logout or account compromise without extra infrastructure.</p>
<p>Common solutions include short-lived access tokens (15 minutes) paired with refresh tokens stored server-side, or a token denylist in a fast store like Redis. Choose the approach that matches your application's requirements.</p>
<p><strong>JWT security checklist</strong>:</p>
<ul>
<li><p>Always pass <code>algorithms: ['HS256']</code> to <code>verify()</code>.</p>
</li>
<li><p>Use a secret of at least 32 random bytes generated with <code>crypto.randomBytes</code>.</p>
</li>
<li><p>Set short expiration times.</p>
</li>
<li><p>Store only non-sensitive identifiers (user ID) in the payload — not emails, passwords, or roles.</p>
</li>
<li><p>Use standard claims: <code>sub</code>, <code>iss</code>, <code>aud</code>, <code>exp</code>.</p>
</li>
<li><p>Plan your revocation strategy before going to production.</p>
</li>
</ul>
<h2 id="heading-summary">Summary</h2>
<p>Here's a quick reference for the six vulnerability categories covered in this tutorial:</p>
<table>
<thead>
<tr>
<th>Vulnerability</th>
<th>Root Cause</th>
<th>Core Fix</th>
</tr>
</thead>
<tbody><tr>
<td>IDOR</td>
<td>Authorization check missing</td>
<td>Verify ownership from the authenticated session</td>
</tr>
<tr>
<td>Mass Assignment</td>
<td>All request body fields applied to database</td>
<td>Allowlist allowed fields explicitly</td>
</tr>
<tr>
<td>Prototype Pollution</td>
<td>Recursive merge of untrusted input</td>
<td>Store state server-side. Use <code>Object.create(null)</code> for merge targets.</td>
</tr>
<tr>
<td>Race Condition</td>
<td>Check-then-act without atomicity</td>
<td>Atomic <code>UPDATE ... WHERE condition</code> or transactions</td>
</tr>
<tr>
<td>Business Logic Flaw</td>
<td>Trusting client-supplied numeric values</td>
<td>Regex-validate inputs. Use integer arithmetic for money.</td>
</tr>
<tr>
<td>JWT Misconfiguration</td>
<td>No algorithm allowlist, weak secrets</td>
<td>Explicit algorithm array, strong random secret, short expiration</td>
</tr>
</tbody></table>
<p>None of these vulnerabilities requires a clever attacker. They just need code that trusts the wrong thing at the wrong time.</p>
<p>The patterns worth internalizing: always derive authorization from the session, never from the request. Validate every numeric input with a range. Make financial writes atomic. Build your JWT configuration explicitly.</p>
<p>Understanding how to fix these vulnerabilities is one side of the picture. Understanding how they're actually identified and exploited during a real security assessment is the other.</p>
<p>If you want to go deeper on the offensive side — how penetration testers approach web application targets, what they look for, and how they chain multiple flaws together — this <a href="https://hackita.it/articoli/attacchi-applicazioni-web/">guide to web application attack techniques</a> covers that perspective in detail.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
