Writing Secure Code
Secure coding is the practice of writing software that is resistant to known vulnerabilities and attack patterns. It is far cheaper and more effective to prevent vulnerabilities during development than to fix them after deployment. Every developer on the team should understand common vulnerability patterns and how to avoid them.
The most dangerous vulnerabilities typically involve untrusted input being processed without proper validation or sanitization. SQL injection, cross-site scripting (XSS), command injection, and path traversal all share the same root cause: mixing untrusted data with trusted code or commands.
Preventing Injection Attacks
Injection attacks occur when untrusted data is sent to an interpreter as part of a command or query. The attacker's hostile data can trick the interpreter into executing unintended commands or accessing unauthorized data. SQL injection remains one of the most common and devastating web application vulnerabilities.
// SQL Injection Prevention
// VULNERABLE: String concatenation
async function findUserUnsafe(email: string) {
// NEVER DO THIS - SQL injection vulnerability
// An attacker could send: ' OR '1'='1' --
const query = `SELECT * FROM users WHERE email = '${email}'`;
return db.query(query);
}
// SAFE: Parameterized queries
async function findUserSafe(email: string) {
// The database driver handles escaping
return db.query("SELECT * FROM users WHERE email = $1", [email]);
}
// SAFE: Using an ORM with parameterized queries
async function findUserORM(email: string) {
return prisma.user.findUnique({
where: { email }, // Prisma parameterizes automatically
});
}
// NoSQL Injection Prevention (MongoDB)
// VULNERABLE: Direct object insertion
async function findUserMongoUnsafe(query: any) {
// Attacker could send: { "$gt": "" } to match all users
return collection.findOne({ username: query.username });
}
// SAFE: Type validation + explicit field selection
async function findUserMongoSafe(username: string) {
if (typeof username !== "string") {
throw new Error("Username must be a string");
}
return collection.findOne(
{ username: { $eq: username } }, // Explicit $eq operator
{ projection: { password: 0 } } // Exclude sensitive fields
);
}
Cross-Site Scripting (XSS) Prevention
XSS attacks inject malicious scripts into web pages viewed by other users. There are three types: Stored XSS (malicious script is stored in the database and served to every user who views the page), Reflected XSS (malicious script is reflected off the server in an error message or search result), and DOM-based XSS (the vulnerability exists in client-side JavaScript that processes untrusted data).
// XSS Prevention: Output Encoding
// HTML entity encoding for rendering user content
function escapeHtml(unsafe: string): string {
return unsafe
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// In React, JSX auto-escapes by default - this is SAFE:
function UserProfile({ user }: { user: User }) {
return <div>{user.name}</div>; // Auto-escaped by React
}
// DANGEROUS: dangerouslySetInnerHTML bypasses React's auto-escaping
function UnsafeComponent({ htmlContent }: { htmlContent: string }) {
// Only use with sanitized content!
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
}
// SAFE: Sanitize HTML before rendering
import DOMPurify from "isomorphic-dompurify";
function SafeHtmlComponent({ htmlContent }: { htmlContent: string }) {
const sanitized = DOMPurify.sanitize(htmlContent, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br", "ul", "li"],
ALLOWED_ATTR: ["href", "target", "rel"],
ALLOW_DATA_ATTR: false,
});
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}
// URL validation to prevent javascript: URLs
function isValidUrl(url: string): boolean {
try {
const parsed = new URL(url);
return ["http:", "https:", "mailto:"].includes(parsed.protocol);
} catch {
return false;
}
}
// Safe link component
function SafeLink({ href, children }: { href: string; children: React.ReactNode }) {
if (!isValidUrl(href)) {
return <span>{children}</span>; // Render as plain text
}
return <a href={href} rel="noopener noreferrer">{children}</a>;
}
Command Injection Prevention
Command injection occurs when an application passes unsafe user data to a system shell. If an attacker can inject shell metacharacters, they can execute arbitrary commands on the server. This is especially dangerous because it can lead to complete server compromise.
import { execFile, spawn } from "child_process";
// VULNERABLE: Using exec with string concatenation
import { exec } from "child_process";
function pingUnsafe(host: string) {
// Attacker could send: "google.com; rm -rf /"
exec(`ping -c 4 ${host}`, (err, stdout) => {
console.log(stdout);
});
}
// SAFE: Use execFile with arguments array (no shell interpolation)
function pingSafe(host: string) {
// Validate input first
if (!/^[a-zA-Z0-9.-]+$/.test(host)) {
throw new Error("Invalid hostname");
}
// execFile does NOT use a shell, so metacharacters are not interpreted
execFile("ping", ["-c", "4", host], (err, stdout) => {
console.log(stdout);
});
}
// SAFE: Using spawn with explicit arguments
function gitLogSafe(repoPath: string, count: number) {
// Validate repoPath is actually a valid path
const normalizedPath = path.resolve(repoPath);
if (!normalizedPath.startsWith("/allowed/repos/")) {
throw new Error("Invalid repository path");
}
return new Promise((resolve, reject) => {
const proc = spawn("git", ["log", `--max-count=${count}`, "--oneline"], {
cwd: normalizedPath,
shell: false, // Explicitly disable shell
});
let output = "";
proc.stdout.on("data", (data) => (output += data));
proc.on("close", (code) => {
if (code === 0) resolve(output);
else reject(new Error(`Git exited with code ${code}`));
});
});
}
Path Traversal Prevention
Path traversal attacks attempt to access files and directories outside the intended directory by using sequences like ../ in file paths. If your application reads or serves files based on user input, you must validate that the resolved path stays within the allowed directory.
import path from "path";
import fs from "fs/promises";
const UPLOAD_DIR = "/app/uploads";
// SAFE: Path traversal prevention
async function readUploadedFile(filename: string): Promise<Buffer> {
// Resolve the full path
const filePath = path.resolve(UPLOAD_DIR, filename);
// Verify the resolved path is within the allowed directory
if (!filePath.startsWith(UPLOAD_DIR + path.sep)) {
throw new SecurityError("Path traversal detected");
}
// Additional checks
const stats = await fs.stat(filePath);
if (!stats.isFile()) {
throw new Error("Not a file");
}
// Check file size to prevent memory issues
if (stats.size > 10 * 1024 * 1024) {
throw new Error("File too large");
}
return fs.readFile(filePath);
}
// Safe file upload handler
async function handleFileUpload(req: Request) {
const file = req.file;
if (!file) throw new Error("No file uploaded");
// Validate file type by content, not just extension
const allowedMimeTypes = ["image/jpeg", "image/png", "image/webp", "application/pdf"];
if (!allowedMimeTypes.includes(file.mimetype)) {
throw new Error("File type not allowed");
}
// Generate a random filename to prevent path manipulation
const ext = path.extname(file.originalname).toLowerCase();
const allowedExtensions = [".jpg", ".jpeg", ".png", ".webp", ".pdf"];
if (!allowedExtensions.includes(ext)) {
throw new Error("File extension not allowed");
}
const safeFilename = `${crypto.randomUUID()}${ext}`;
const destPath = path.join(UPLOAD_DIR, safeFilename);
await fs.writeFile(destPath, file.buffer);
return safeFilename;
}
Security Warning: Common Coding Mistakes
- Using eval(): Never use eval(), new Function(), or setTimeout/setInterval with strings. These execute arbitrary code.
- Serialization vulnerabilities: Never deserialize untrusted data with libraries that support code execution (YAML, pickle). Use JSON.parse for untrusted data.
- Prototype pollution: Deep merge and object assignment from untrusted data can pollute Object.prototype. Validate and sanitize object keys.
- RegEx DoS (ReDoS): Carefully crafted input can cause catastrophic backtracking in regular expressions. Use RE2 for user-facing regex or set timeouts.
Secure Coding Principles
- Input validation: Validate all input on the server side. Use allowlists, not denylists.
- Output encoding: Encode output appropriate to the context (HTML, URL, JavaScript, CSS).
- Parameterized queries: Never concatenate user input into SQL, LDAP, or OS commands.
- Principle of least privilege: Run processes with minimum required permissions.
- Fail securely: Default to denying access when errors occur.
- Keep it simple: Complex code is harder to audit and more likely to contain vulnerabilities.