Understand the browser security model, common web vulnerabilities, authentication risks, secure data handling, and defensive practices for modern frontend applications.
Explain how browsers enforce origins and trust boundaries
Recognize common vulnerabilities such as XSS, CSRF, and injection
Handle authentication, sessions, cookies, and tokens safely
Apply secure input validation and output encoding
Use browser security controls such as CSP, CORS, and security headers
Avoid exposing secrets or sensitive data in frontend applications
Web security starts by identifying which data, systems, and actors can be trusted. Data received from users, URLs, APIs, storage, third-party scripts, and external services should be treated as untrusted until validated.
A trust boundary exists whenever data crosses between systems with different security assumptions, such as from a browser to an API or from an API to a database.
Treat external input as untrusted
Validate data when it crosses a trust boundary
Apply least privilege to users, services, and credentials
import { z } from "zod";
const ProfileSchema = z.object({
name: z.string().trim().min(1).max(80),
age: z.number().int().min(13).max(120),
});
export function parseProfile(input: unknown) {
const result = ProfileSchema.safeParse(input);
if (!result.success) {
throw new Error("Invalid profile data");
}
return result.data;
}An origin is defined by a URL's scheme, host, and port. Browsers use origins as an important security boundary between websites.
The same-origin policy limits one origin from directly reading sensitive data belonging to another origin unless an explicit mechanism permits the interaction.
Scheme, host, and port determine an origin
Same-origin restrictions protect data between websites
Cross-origin permission should be granted deliberately
const first = new URL("https://app.example.com:443/profile");
const second = new URL("https://api.example.com:443/users");
console.log(first.origin);
// https://app.example.com
console.log(second.origin);
// https://api.example.com
console.log(first.origin === second.origin);
// falseCORS is a browser mechanism that lets a server declare which origins may read its responses through browser-based cross-origin requests.
CORS is not an authentication system. Allowing an origin does not determine whether the caller is authorized to access a protected resource.
The server decides which origins receive cross-origin access
CORS controls browser response access rather than user authorization
Avoid reflecting arbitrary origins into access-control headers
const allowedOrigins = new Set([
"https://app.example.com",
]);
function getCorsOrigin(origin: string | null) {
if (!origin || !allowedOrigins.has(origin)) {
return null;
}
return origin;
}Cross-site scripting occurs when untrusted data is interpreted as executable browser content. Successful XSS can allow attacker-controlled JavaScript to run in the security context of an application.
Modern frameworks escape ordinary text rendering by default, but unsafe HTML APIs, DOM manipulation, and untrusted URLs can reintroduce XSS risks.
Keep untrusted data in text contexts
Avoid unsafe HTML insertion unless content is sanitized
Do not build executable code from user-controlled strings
type CommentProps = {
author: string;
message: string;
};
export function Comment({ author, message }: CommentProps) {
return (
<article>
<strong>{author}</strong>
<p>{message}</p>
</article>
);
}
// React escapes interpolated text by default.
// Avoid injecting untrusted HTML with
// dangerouslySetInnerHTML.Injection vulnerabilities occur when untrusted input becomes part of a command or query and is interpreted as executable syntax rather than ordinary data.
Parameterized queries keep SQL instructions separate from user-provided values and are the standard defense against SQL injection.
Separate commands from data
Use parameterized database queries
Do not construct SQL with string concatenation
const email = request.body.email;
const result = await db.query(
`SELECT id, email, display_name
FROM users
WHERE email = $1`,
[email],
);
return result.rows[0] ?? null;CSRF attempts to cause a user's browser to submit an unwanted authenticated request to another site. It is especially relevant when credentials such as cookies are automatically attached to requests.
SameSite cookies, CSRF tokens, origin validation, and requiring appropriate request methods can reduce CSRF risk.
CSRF abuses automatically included credentials
Use SameSite cookie policies where appropriate
Protect state-changing operations with additional request validation
response.cookie("session", sessionId, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: 1000 * 60 * 60,
});Authentication establishes who a user is. Authorization determines what an authenticated user is allowed to do.
Authorization must be enforced on trusted server-side boundaries. Hiding a button in the frontend improves user experience but does not prevent a caller from sending a request directly.
Authentication answers who the caller is
Authorization answers what the caller may do
Enforce permissions on the server
async function deleteProject(
user: User,
projectId: string,
) {
const project = await projects.findById(projectId);
if (!project) {
throw new Error("Project not found");
}
if (project.ownerId !== user.id) {
throw new Error("Forbidden");
}
await projects.delete(projectId);
}Authentication state can be represented through server-side sessions, cookies, or signed tokens. Each approach has different storage and lifecycle considerations.
Sensitive session cookies should generally use HttpOnly and Secure attributes so JavaScript cannot directly access them and browsers send them only over HTTPS.
Protect authentication credentials from unnecessary JavaScript access
Use short, controlled credential lifetimes
Revoke or rotate credentials when security-sensitive state changes
const cookieOptions = {
httpOnly: true,
secure: true,
sameSite: "lax" as const,
path: "/",
};
response.cookie(
"session",
session.id,
cookieOptions,
);Applications should never store plaintext passwords. Passwords should be processed with a password-hashing algorithm designed to be deliberately expensive to compute.
Authentication systems should also support protections such as rate limiting, secure reset flows, and multi-factor authentication where appropriate.
Never store plaintext passwords
Use established password-hashing libraries
Protect login and password-reset endpoints from abuse
import argon2 from "argon2";
export async function createPasswordHash(
password: string,
) {
return argon2.hash(password);
}
export async function verifyPassword(
hash: string,
password: string,
) {
return argon2.verify(hash, password);
}Code delivered to a browser must be considered visible to the user. API keys, database credentials, private signing keys, and other true secrets must not be embedded in frontend bundles.
Environment variables used during frontend builds are not automatically private. A value exposed to browser code can be inspected after deployment.
Never place server secrets in browser-delivered code
Keep privileged credentials on trusted servers
Treat public client identifiers differently from true secrets
// server/payment-service.ts
const secretKey = process.env.PAYMENT_SECRET_KEY;
if (!secretKey) {
throw new Error("Missing payment secret");
}
export async function createCheckout() {
// Privileged API call happens on the server.
return paymentClient.createSession({
secretKey,
});
}Content Security Policy lets a site restrict which sources may provide scripts, styles, images, frames, and other resources.
A strong CSP reduces the impact of certain injection vulnerabilities by limiting which code the browser is permitted to execute.
Use CSP as defense in depth
Restrict script sources whenever practical
Prefer nonces or hashes over broadly allowing inline scripts
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
object-src 'none';
frame-ancestors 'none';HTTPS protects data in transit between the browser and server from passive observation and tampering on the network.
Security headers can further control browser behavior, including transport security, framing, content-type interpretation, and referrer information.
Use HTTPS for production applications
Enable appropriate security headers
Avoid relying on transport encryption as the only security control
response.setHeader(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains",
);
response.setHeader(
"X-Content-Type-Options",
"nosniff",
);
response.setHeader(
"Referrer-Policy",
"strict-origin-when-cross-origin",
);