Cross-Site Request Forgery (CSRF) and injection attacks exploit different trust boundaries. CSRF abuses the browser's automatic cookie-sending behavior. Injection abuses the server's trust in user-supplied data. Both are preventable with well-known patterns.
Cross-Site Request Forgery (CSRF)
How It Works
When a user is logged into bank.com, their browser automatically sends cookies with every request to that domain. An attacker can trick the user into making a request they did not intend:
<!-- Attacker's page at evil.com -->
<!-- The victim visits this page while logged into bank.com -->
<form actio="https://bank.com/transfer" metho="POST" i="csrf-form">
<input typ="hidden" nam="to" valu="attacker" />
<input typ="hidden" nam="amount" valu="10000" />
</form>
<script>document.getElementById("csrf-form").submit();</script>
<!-- The browser sends the victim's bank.com cookies automatically. -->
<!-- The server sees a valid session and processes the transfer. -->Prevention: CSRF Tokens
Include a unique, unpredictable token in every state-changing form. The server validates it before processing the request.
import crypto from "crypto";
// Generate a CSRF token and store it in the session
function generateCsrfToken(session: SessionData): string {
const token = crypto.randomBytes(32).toString("hex");
session.csrfToken = token;
return token;
}
// Validate the token on form submission
function validateCsrfToken(session: SessionData, token: string): boolean {
if (!session.csrfToken || !token) return false;
return crypto.timingSafeEqual(
Buffer.from(session.csrfToken),
Buffer.from(token)
);
}<!-- Include the token in the form -->
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="{{csrfToken}}" />
<input type="text" name="to" />
<input type="number" name="amount" />
<button type="submit">Transfer</button>
</form>Prevention: SameSite Cookies
The SameSite attribute tells the browser when to include cookies in cross-origin requests.
// SECURE: SameSite=Strict prevents the cookie from being sent on ANY cross-site request
const headers = new Headers();
headers.append(
"Set-Cookie",
"session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/"
);
// SameSite=Lax (default in modern browsers) allows cookies on top-level
// navigations (GET requests) but blocks them on cross-origin POST requests.
// This stops the CSRF form attack above.
| SameSite Value | Cross-Site GET | Cross-Site POST | Best For |
|---|---|---|---|
Strict | Blocked | Blocked | Banking, sensitive actions |
Lax | Allowed | Blocked | General-purpose sessions |
None | Allowed | Allowed | Cross-site APIs (requires Secure) |
Prevention: Verify Origin Headers
// Check the Origin or Referer header matches your domain
function verifyCsrfOrigin(request: Request, allowedOrigin: string): boolean {
const origin = request.headers.get("origin");
const referer = request.headers.get("referer");
if (origin) return origin === allowedOrigin;
if (referer) return referer.startsWith(allowedOrigin);
// No origin or referer — reject the request to be safe
return false;
}SQL Injection
How It Works
SQL injection occurs when user input is concatenated directly into a SQL query.
// VULNERABLE: string concatenation builds a dynamic query
async function getUser(username: string) {
const query = `SELECT * FROM users WHERE username = '${username}'`;
return db.query(query);
}
// Attacker input: ' OR '1'='1' --
// Resulting query: SELECT * FROM users WHERE username = '' OR '1'='1' --'
// This returns ALL users in the database.
// Worse: ' ; DROP TABLE users; --
// Resulting query: SELECT * FROM users WHERE username = ''; DROP TABLE users; --'
Prevention: Parameterized Queries
Never build SQL strings with concatenation. Use parameterized queries (also called prepared statements).
// SECURE: parameterized query — the database treats $1 as data, never as SQL
async function getUser(username: string) {
const query = "SELECT * FROM users WHERE username = $1";
return db.query(query, [username]);
}
// Even if username is "' OR '1'='1' --", the database searches for
// a literal username with that exact string. No injection is possible.
This works with every database library:
// Prisma (ORM) — parameterized by default
const user = await prisma.user.findUnique({
where: { username },
});
// Drizzle ORM — parameterized by default
const user = await db
.select()
.from(users)
.where(eq(users.username, username));
// Raw SQL with tagged template literals (e.g., postgres.js)
const user = await sql`SELECT * FROM users WHERE username = ${username}`;Command Injection
How It Works
If your server executes shell commands with user input, attackers can chain additional commands.
// VULNERABLE: user input goes directly into a shell command
import { exec } from "child_process";
app.get("/ping", (req, res) => {
const host = req.query.host;
exec(`ping -c 4 ${host}`, (error, stdout) => {
res.send(stdout);
});
});
// Attacker input: 127.0.0.1; cat /etc/passwd
// Executed: ping -c 4 127.0.0.1; cat /etc/passwd
Prevention
// SECURE: use execFile with an argument array (no shell interpretation)
import { execFile } from "child_process";
app.get("/ping", (req, res) => {
const host = req.query.host;
// Validate input first
if (!/^[\w.\-]+$/.test(host)) {
return res.status(400).send("Invalid hostname");
}
// execFile does NOT spawn a shell — arguments are passed directly
execFile("ping", ["-c", "4", host], (error, stdout) => {
res.send(stdout);
});
});Rules of thumb:
- Avoid
exec— preferexecFileorspawnwith explicit argument arrays. - Validate strictly — allowlist acceptable characters.
- Avoid shell commands entirely when a library can do the job (e.g., use
dns.resolveinstead ofping).
NoSQL Injection
NoSQL databases like MongoDB can also be injected if query operators come from user input.
// VULNERABLE: attacker sends { "username": { "$gt": "" }, "password": { "$gt": "" } }
app.post("/login", async (req, res) => {
const user = await db.collection("users").findOne({
username: req.body.username,
password: req.body.password,
});
// This matches ANY user because $gt: "" is true for all non-empty strings.
});
// SECURE: explicitly cast to string
app.post("/login", async (req, res) => {
const user = await db.collection("users").findOne({
username: String(req.body.username),
password: String(req.body.password), // Also hash passwords — see lesson 04
});
});Key Takeaways
- CSRF exploits the browser's automatic cookie behavior. Defend with tokens,
SameSite, and origin checks. - SQL injection exploits string concatenation in queries. Always use parameterized queries.
- Command injection exploits shell execution. Use
execFilewith argument arrays and strict validation. - The common thread: never trust user input. Validate, sanitize, and use APIs that separate code from data.