Frontend Security Implementation Practices
1. XSS Prevention DOMPurify Sanitization
| Attack Type | Prevention Method | Implementation | Example |
|---|---|---|---|
| Stored XSS | Sanitize on input/output | DOMPurify, validator.js | Clean user-generated content |
| Reflected XSS | Escape user input | Template literals, React escape | URL parameters, search queries |
| DOM-based XSS | Avoid dangerous APIs | Avoid innerHTML, eval, document.write | Use textContent, createElement |
| CSP Headers | Restrict script sources | Content-Security-Policy header | Prevent inline scripts |
| HTTP-only Cookies | Prevent JS access | Set HttpOnly flag | Protect session tokens |
| React dangerouslySetInnerHTML | Sanitize before use | DOMPurify + dangerouslySetInnerHTML | Rich text editors |
Example: XSS prevention with DOMPurify
// Install DOMPurify
npm install dompurify
npm install --save-dev @types/dompurify
// Sanitize user input with DOMPurify
import DOMPurify from 'dompurify';
function SafeHTML({ html }) {
const cleanHTML = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'title', 'target'],
});
return (
<div
dangerouslySetInnerHTML={{ __html: cleanHTML }}
/>
);
}
// Usage
const userContent = '<script>alert("XSS")</script><p>Safe content</p>';
<SafeHTML html={userContent} />
// Output: <p>Safe content</p> (script removed)
// React custom hook for sanitization
import { useMemo } from 'react';
import DOMPurify from 'dompurify';
function useSanitizedHTML(html: string) {
return useMemo(() => DOMPurify.sanitize(html), [html]);
}
// Usage
function RichTextDisplay({ content }) {
const cleanContent = useSanitizedHTML(content);
return <div dangerouslySetInnerHTML={{ __html: cleanContent }} />;
}
// Strict sanitization (remove all HTML)
const strictClean = DOMPurify.sanitize(html, { ALLOWED_TAGS: [] });
// Custom sanitization with hooks
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
// Force all links to open in new tab
if (node.tagName === 'A') {
node.setAttribute('target', '_blank');
node.setAttribute('rel', 'noopener noreferrer');
}
});
// Avoid dangerous DOM APIs
// ❌ Dangerous - XSS vulnerable
element.innerHTML = userInput;
eval(userInput);
document.write(userInput);
new Function(userInput)();
// ✅ Safe alternatives
element.textContent = userInput;
element.innerText = userInput;
const node = document.createTextNode(userInput);
element.appendChild(node);
// React automatically escapes
function SafeComponent({ userInput }) {
// React escapes this automatically
return <div>{userInput}</div>;
}
// Validate and sanitize URL inputs
import validator from 'validator';
function validateURL(url: string) {
if (!validator.isURL(url, { protocols: ['http', 'https'] })) {
throw new Error('Invalid URL');
}
// Additional check for javascript: protocol
if (url.toLowerCase().startsWith('javascript:')) {
throw new Error('JavaScript URLs not allowed');
}
return url;
}
// Safe link component
function SafeLink({ href, children }) {
const [isValid, setIsValid] = useState(false);
useEffect(() => {
try {
validateURL(href);
setIsValid(true);
} catch {
setIsValid(false);
}
}, [href]);
if (!isValid) {
return <span>{children}</span>;
}
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
);
}
// Content Security Policy in Next.js
// next.config.js
const ContentSecurityPolicy = `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
`;
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim(),
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
],
},
];
},
};
// Server-side sanitization (Node.js)
const express = require('express');
const { body, validationResult } = require('express-validator');
const DOMPurify = require('isomorphic-dompurify');
app.post('/comment',
body('content').trim().escape(),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const cleanContent = DOMPurify.sanitize(req.body.content);
// Save cleanContent to database
}
);
// Vue 3 safe rendering
<template>
<!-- Safe by default -->
<div>{{ userInput }}</div>
<!-- Dangerous - sanitize first -->
<div v-html="sanitizedHTML"></div>
</template>
<script setup>
import DOMPurify from 'dompurify';
import { computed } from 'vue';
const props = defineProps(['userInput']);
const sanitizedHTML = computed(() =>
DOMPurify.sanitize(props.userInput)
);
</script>
// Angular safe HTML pipe
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import DOMPurify from 'dompurify';
@Pipe({ name: 'safeHtml' })
export class SafeHtmlPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}
transform(html: string): SafeHtml {
const cleanHTML = DOMPurify.sanitize(html);
return this.sanitizer.bypassSecurityTrustHtml(cleanHTML);
}
}
// Usage
<div [innerHTML]="userContent | safeHtml"></div>
2. CSRF Protection SameSite Cookies
| Protection Method | Implementation | Description | Browser Support |
|---|---|---|---|
| SameSite Cookie | Set-Cookie: SameSite=Strict |
Prevent cross-site cookie sending | All modern browsers |
| CSRF Token | Synchronizer token pattern | Unique token per session/request | Universal |
| Double Submit Cookie | Token in cookie + request header | Compare values server-side | Universal |
| Custom Header | X-Requested-With: XMLHttpRequest |
CORS prevents cross-origin | AJAX requests only |
| Origin Header Check | Verify Origin/Referer header | Validate request source | Server-side validation |
Example: CSRF protection implementation
// Set SameSite cookies (Express.js)
const express = require('express');
const session = require('express-session');
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict', // or 'lax' for better UX
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
}));
// SameSite options:
// - Strict: Cookie not sent on any cross-site request
// - Lax: Cookie sent on top-level navigation (GET)
// - None: Cookie sent on all requests (requires Secure flag)
// CSRF Token with csurf middleware
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
app.use(cookieParser());
app.use(csrf({ cookie: true }));
// Send token to client
app.get('/form', (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
// Validate token on POST
app.post('/submit', (req, res) => {
// Token automatically validated by middleware
// If invalid, 403 error is thrown
res.json({ success: true });
});
// React CSRF token implementation
// Store token in meta tag (server-rendered)
<head>
<meta name="csrf-token" content="{{ csrfToken }}" />
</head>
// Read and include in requests
function getCSRFToken() {
const meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : '';
}
// Axios interceptor for CSRF token
import axios from 'axios';
axios.interceptors.request.use((config) => {
const token = getCSRFToken();
if (token) {
config.headers['X-CSRF-Token'] = token;
}
return config;
});
// Fetch with CSRF token
async function submitForm(data) {
const response = await fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCSRFToken(),
},
body: JSON.stringify(data),
credentials: 'same-origin', // Include cookies
});
return response.json();
}
// React hook for CSRF protection
function useCSRFToken() {
const [token, setToken] = useState('');
useEffect(() => {
// Fetch token from server
fetch('/api/csrf-token')
.then(res => res.json())
.then(data => setToken(data.token));
}, []);
return token;
}
// Usage
function SecureForm() {
const csrfToken = useCSRFToken();
const handleSubmit = async (data) => {
await fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify(data),
});
};
return <form onSubmit={handleSubmit}>...</form>;
}
// Next.js API route with CSRF protection
// pages/api/submit.js
import { csrf } from 'lib/csrf';
export default csrf(async (req, res) => {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
// Token validated by middleware
// Process request
res.json({ success: true });
});
// Custom CSRF middleware
// lib/csrf.js
import { randomBytes } from 'crypto';
const tokens = new Map();
export function generateCSRFToken(sessionId) {
const token = randomBytes(32).toString('hex');
tokens.set(sessionId, token);
return token;
}
export function validateCSRFToken(sessionId, token) {
const storedToken = tokens.get(sessionId);
return storedToken && storedToken === token;
}
export function csrf(handler) {
return async (req, res) => {
if (req.method === 'POST') {
const token = req.headers['x-csrf-token'];
const sessionId = req.cookies.sessionId;
if (!validateCSRFToken(sessionId, token)) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
}
return handler(req, res);
};
}
// Double Submit Cookie pattern
app.post('/api/submit', (req, res) => {
const cookieToken = req.cookies.csrfToken;
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF validation failed' });
}
// Process request
});
// Origin header validation
app.use((req, res, next) => {
const origin = req.headers.origin || req.headers.referer;
const allowedOrigins = [
'https://example.com',
'https://www.example.com',
];
if (req.method !== 'GET' && req.method !== 'HEAD') {
if (!origin || !allowedOrigins.some(allowed => origin.startsWith(allowed))) {
return res.status(403).json({ error: 'Invalid origin' });
}
}
next();
});
// Custom header check (AJAX only)
app.use((req, res, next) => {
if (req.method !== 'GET' && req.method !== 'HEAD') {
const xhr = req.headers['x-requested-with'] === 'XMLHttpRequest';
if (!xhr) {
return res.status(403).json({ error: 'Invalid request' });
}
}
next();
});
// React Context for CSRF token
const CSRFContext = createContext('');
export function CSRFProvider({ children }) {
const [token, setToken] = useState('');
useEffect(() => {
fetch('/api/csrf-token')
.then(res => res.json())
.then(data => setToken(data.token));
}, []);
return (
<CSRFContext.Provider value={token}>
{children}
</CSRFContext.Provider>
);
}
export const useCSRF = () => useContext(CSRFContext);
3. Content Security Policy Headers
| Directive | Purpose | Example Value | Protection |
|---|---|---|---|
| default-src | Fallback for all directives | 'self' |
Restrict all resources |
| script-src | JavaScript sources | 'self' 'nonce-{random}' |
Prevent inline scripts |
| style-src | CSS sources | 'self' 'unsafe-inline' |
Control stylesheets |
| img-src | Image sources | 'self' data: https: |
Restrict image loading |
| connect-src | XHR, WebSocket, fetch | 'self' https://api.example.com |
Control API endpoints |
| frame-ancestors | Embedding pages | 'none' |
Prevent clickjacking |
Example: Comprehensive CSP implementation
// Strict CSP policy
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}';
style-src 'self' 'nonce-{random}';
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self' https://api.example.com;
frame-src 'none';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
block-all-mixed-content;
// Express.js CSP middleware
const helmet = require('helmet');
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'", "data:"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
frameAncestors: ["'none'"],
upgradeInsecureRequests: [],
},
})
);
// Next.js CSP with nonce
// next.config.js
const crypto = require('crypto');
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: generateCSP(),
},
],
},
];
},
};
function generateCSP() {
const nonce = crypto.randomBytes(16).toString('base64');
const csp = {
'default-src': ["'self'"],
'script-src': ["'self'", `'nonce-${nonce}'`, "'strict-dynamic'"],
'style-src': ["'self'", "'unsafe-inline'"],
'img-src': ["'self'", "data:", "https:"],
'font-src': ["'self'", "data:"],
'connect-src': ["'self'", "https://api.example.com"],
'frame-ancestors': ["'none'"],
'base-uri': ["'self'"],
'form-action': ["'self'"],
};
return Object.entries(csp)
.map(([key, values]) => `${key} ${values.join(' ')}`)
.join('; ');
}
// Use nonce in scripts
<script nonce="{nonce}">
console.log('Allowed script');
</script>
// React Helmet for CSP
import { Helmet } from 'react-helmet';
function App() {
return (
<>
<Helmet>
<meta
httpEquiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'unsafe-inline'"
/>
</Helmet>
<div>App content</div>
</>
);
}
// CSP Report-Only mode (testing)
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
// CSP violation reporting endpoint
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('CSP Violation:', req.body);
// Log to monitoring service
res.status(204).end();
});
// CSP violation handler (client-side)
document.addEventListener('securitypolicyviolation', (e) => {
console.error('CSP Violation:', {
blockedURI: e.blockedURI,
violatedDirective: e.violatedDirective,
originalPolicy: e.originalPolicy,
});
// Send to analytics
fetch('/api/csp-violation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
blockedURI: e.blockedURI,
violatedDirective: e.violatedDirective,
}),
});
});
// Next.js 13 App Router with CSP
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`.replace(/\s{2,}/g, ' ').trim();
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
requestHeaders.set('Content-Security-Policy', cspHeader);
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
response.headers.set('Content-Security-Policy', cspHeader);
return response;
}
// Vite CSP plugin
// vite.config.ts
import { defineConfig } from 'vite';
import { createHtmlPlugin } from 'vite-plugin-html';
export default defineConfig({
plugins: [
createHtmlPlugin({
inject: {
data: {
csp: "default-src 'self'; script-src 'self' 'unsafe-inline'",
},
},
}),
],
server: {
headers: {
'Content-Security-Policy': "default-src 'self'; script-src 'self'",
},
},
});
// Additional security headers
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=()');
next();
});
4. JWT Token HttpOnly Storage
| Storage Method | Security Level | Pros | Cons |
|---|---|---|---|
| HttpOnly Cookie | 🟢 High | XSS protection, automatic sending | CSRF risk (mitigated with SameSite) |
| localStorage | 🔴 Low | Easy access, persists | XSS vulnerable, no auto-sending |
| sessionStorage | 🟡 Medium | Tab-scoped, clears on close | XSS vulnerable |
| Memory (React state) | 🟢 High | XSS resistant, cleared on refresh | Lost on refresh, needs refresh token |
| Secure Cookie + CSRF Token | 🟢 Highest | XSS + CSRF protection | Complex implementation |
Example: Secure JWT token storage and handling
// ✅ RECOMMENDED: HttpOnly cookie with refresh token
// Server-side (Express.js)
const jwt = require('jsonwebtoken');
// Login endpoint
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
// Validate credentials
const user = await validateUser(email, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate access token (short-lived)
const accessToken = jwt.sign(
{ userId: user.id, email: user.email },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
// Generate refresh token (long-lived)
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);
// Store refresh token in database
await storeRefreshToken(user.id, refreshToken);
// Set HttpOnly cookies
res.cookie('accessToken', accessToken, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict',
maxAge: 15 * 60 * 1000, // 15 minutes
});
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: '/auth/refresh', // Only sent to refresh endpoint
});
res.json({ success: true, user: { id: user.id, email: user.email } });
});
// Verify token middleware
function authenticateToken(req, res, next) {
const token = req.cookies.accessToken;
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid token' });
}
req.user = user;
next();
});
}
// Protected route
app.get('/api/profile', authenticateToken, (req, res) => {
res.json({ user: req.user });
});
// Refresh token endpoint
app.post('/auth/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'No refresh token' });
}
// Verify refresh token
jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, async (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid refresh token' });
}
// Check if token exists in database
const storedToken = await getRefreshToken(user.userId);
if (storedToken !== refreshToken) {
return res.status(403).json({ error: 'Token revoked' });
}
// Generate new access token
const newAccessToken = jwt.sign(
{ userId: user.userId },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
res.cookie('accessToken', newAccessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000,
});
res.json({ success: true });
});
});
// Logout endpoint
app.post('/auth/logout', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
// Remove from database
if (refreshToken) {
await revokeRefreshToken(refreshToken);
}
// Clear cookies
res.clearCookie('accessToken');
res.clearCookie('refreshToken');
res.json({ success: true });
});
// Client-side (React)
// Auth context with automatic refresh
import { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check authentication on mount
checkAuth();
// Set up automatic token refresh
const interval = setInterval(() => {
refreshAccessToken();
}, 14 * 60 * 1000); // Refresh every 14 minutes
return () => clearInterval(interval);
}, []);
const checkAuth = async () => {
try {
const response = await fetch('/api/profile', {
credentials: 'include', // Include cookies
});
if (response.ok) {
const data = await response.json();
setUser(data.user);
}
} catch (error) {
console.error('Auth check failed:', error);
} finally {
setLoading(false);
}
};
const refreshAccessToken = async () => {
try {
await fetch('/auth/refresh', {
method: 'POST',
credentials: 'include',
});
} catch (error) {
console.error('Token refresh failed:', error);
setUser(null);
}
};
const login = async (email, password) => {
const response = await fetch('/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
setUser(data.user);
return true;
}
return false;
};
const logout = async () => {
await fetch('/auth/logout', {
method: 'POST',
credentials: 'include',
});
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout, loading }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);
// Axios interceptor for automatic token refresh
import axios from 'axios';
axios.defaults.withCredentials = true;
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
axios.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(() => axios(originalRequest));
}
originalRequest._retry = true;
isRefreshing = true;
try {
await axios.post('/auth/refresh');
processQueue(null);
return axios(originalRequest);
} catch (refreshError) {
processQueue(refreshError);
// Redirect to login
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
// Next.js middleware for token validation
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose';
export async function middleware(request: NextRequest) {
const token = request.cookies.get('accessToken')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
try {
await jwtVerify(
token,
new TextEncoder().encode(process.env.ACCESS_TOKEN_SECRET!)
);
return NextResponse.next();
} catch {
return NextResponse.redirect(new URL('/login', request.url));
}
}
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*'],
};
5. Input Validation Joi Yup Schemas
| Library | Ecosystem | Features | Use Case |
|---|---|---|---|
| Joi | Node.js backend | Rich schema, custom messages | API validation |
| Yup | React frontend | Schema validation, TypeScript | Form validation |
| Zod | Full-stack TypeScript | Type inference, parse | tRPC, type-safe APIs |
| express-validator | Express.js | Middleware chain | Request validation |
| class-validator | NestJS, TypeScript | Decorator-based | DTO validation |
| Ajv | JSON Schema | Fast, standard-based | Configuration validation |
Example: Comprehensive input validation
// Joi validation (Backend/Node.js)
npm install joi
const Joi = require('joi');
// Define schema
const userSchema = Joi.object({
username: Joi.string()
.alphanum()
.min(3)
.max(30)
.required()
.messages({
'string.min': 'Username must be at least 3 characters',
'any.required': 'Username is required',
}),
email: Joi.string()
.email({ minDomainSegments: 2 })
.required(),
password: Joi.string()
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/)
.required()
.messages({
'string.pattern.base': 'Password must contain uppercase, lowercase, number, and special character',
}),
age: Joi.number()
.integer()
.min(18)
.max(120)
.optional(),
website: Joi.string()
.uri()
.optional(),
birthdate: Joi.date()
.iso()
.max('now')
.optional(),
role: Joi.string()
.valid('user', 'admin', 'moderator')
.default('user'),
});
// Validate in Express route
app.post('/api/users', async (req, res) => {
try {
const value = await userSchema.validateAsync(req.body, {
abortEarly: false, // Return all errors
});
// Value is validated and sanitized
const user = await createUser(value);
res.json(user);
} catch (error) {
if (error.isJoi) {
return res.status(400).json({
errors: error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message,
})),
});
}
res.status(500).json({ error: 'Internal server error' });
}
});
// Yup validation (Frontend/React)
npm install yup
import * as yup from 'yup';
// Define schema
const loginSchema = yup.object({
email: yup
.string()
.email('Invalid email address')
.required('Email is required'),
password: yup
.string()
.min(8, 'Password must be at least 8 characters')
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
'Password must contain uppercase, lowercase, and number'
)
.required('Password is required'),
rememberMe: yup.boolean().default(false),
});
// React Hook Form with Yup
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: yupResolver(loginSchema),
});
const onSubmit = async (data) => {
// Data is validated
await login(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register('password')} />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">Login</button>
</form>
);
}
// Zod validation (Full-stack TypeScript)
npm install zod
import { z } from 'zod';
// Define schema with type inference
const userSchema = z.object({
username: z.string().min(3).max(30),
email: z.string().email(),
password: z.string().min(8),
age: z.number().int().min(18).optional(),
role: z.enum(['user', 'admin', 'moderator']).default('user'),
});
// TypeScript type automatically inferred
type User = z.infer<typeof userSchema>;
// Validate
const result = userSchema.safeParse(data);
if (!result.success) {
console.error(result.error.issues);
} else {
const user: User = result.data;
}
// tRPC with Zod
import { router, publicProcedure } from './trpc';
export const userRouter = router({
create: publicProcedure
.input(userSchema)
.mutation(async ({ input }) => {
// input is typed as User
return await createUser(input);
}),
});
// express-validator
npm install express-validator
const { body, validationResult } = require('express-validator');
app.post('/api/users',
// Validation middleware
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }),
body('username').trim().isLength({ min: 3, max: 30 }).escape(),
// Handler
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process validated data
const user = createUser(req.body);
res.json(user);
}
);
// Custom validation with Yup
const passwordMatchSchema = yup.object({
password: yup.string().required(),
confirmPassword: yup
.string()
.oneOf([yup.ref('password')], 'Passwords must match')
.required(),
});
// Conditional validation
const addressSchema = yup.object({
country: yup.string().required(),
state: yup.string().when('country', {
is: 'US',
then: (schema) => schema.required('State is required for US'),
otherwise: (schema) => schema.optional(),
}),
});
// Array validation
const orderSchema = yup.object({
items: yup.array().of(
yup.object({
productId: yup.string().required(),
quantity: yup.number().positive().integer().required(),
price: yup.number().positive().required(),
})
).min(1, 'At least one item required'),
});
// File validation
const uploadSchema = yup.object({
file: yup
.mixed()
.required('File is required')
.test('fileSize', 'File too large', (value) => {
return value && value.size <= 5000000; // 5MB
})
.test('fileType', 'Invalid file type', (value) => {
return value && ['image/jpeg', 'image/png'].includes(value.type);
}),
});
// Sanitization helpers
function sanitizeInput(input) {
return input
.trim()
.replace(/[<>"'\/]/g, (char) => {
const entities = {
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
};
return entities[char];
});
}
// SQL injection prevention with parameterized queries
// ❌ Vulnerable
const query = `SELECT * FROM users WHERE email = '${email}'`;
// ✅ Safe
const query = 'SELECT * FROM users WHERE email = ?';
db.query(query, [email]);
// NoSQL injection prevention
// ❌ Vulnerable
const user = await User.findOne({ email: req.body.email });
// ✅ Safe with validation
const emailSchema = yup.string().email().required();
const email = await emailSchema.validate(req.body.email);
const user = await User.findOne({ email });
6. HTTPS Certificate Pinning Security
| Security Measure | Implementation | Purpose | Level |
|---|---|---|---|
| HTTPS/TLS | SSL/TLS certificate | Encrypt data in transit | Essential |
| Certificate Pinning | Pin public key/certificate | Prevent MITM attacks | Advanced |
| HSTS | Strict-Transport-Security header | Force HTTPS | Recommended |
| TLS 1.3 | Modern protocol version | Latest encryption standards | Best practice |
| Certificate Transparency | CT logs monitoring | Detect rogue certificates | Advanced |
| Secure Ciphers | Strong cipher suites | Prevent weak encryption | Essential |
Example: HTTPS and security configuration
// HTTPS server setup (Node.js)
const https = require('https');
const fs = require('fs');
const express = require('express');
const app = express();
const options = {
key: fs.readFileSync('/path/to/private-key.pem'),
cert: fs.readFileSync('/path/to/certificate.pem'),
ca: fs.readFileSync('/path/to/ca-certificate.pem'),
// TLS options
minVersion: 'TLSv1.3',
ciphers: [
'TLS_AES_128_GCM_SHA256',
'TLS_AES_256_GCM_SHA384',
'TLS_CHACHA20_POLY1305_SHA256',
].join(':'),
honorCipherOrder: true,
};
https.createServer(options, app).listen(443);
// HSTS (HTTP Strict Transport Security)
app.use((req, res, next) => {
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
next();
});
// Redirect HTTP to HTTPS
const http = require('http');
http.createServer((req, res) => {
res.writeHead(301, {
Location: `https://${req.headers.host}${req.url}`,
});
res.end();
}).listen(80);
// Next.js security headers
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN',
},
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
],
},
];
},
};
// Helmet.js for Express security headers
npm install helmet
const helmet = require('helmet');
app.use(helmet({
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
},
},
frameguard: {
action: 'deny',
},
}));
// Certificate pinning (React Native / Mobile)
// Note: Not commonly used in web browsers, mainly mobile apps
const fetch = require('node-fetch');
const https = require('https');
const crypto = require('crypto');
const expectedFingerprint = 'AA:BB:CC:DD:EE:FF...';
const agent = new https.Agent({
checkServerIdentity: (hostname, cert) => {
const fingerprint = crypto
.createHash('sha256')
.update(cert.raw)
.digest('hex')
.toUpperCase()
.match(/.{2}/g)
.join(':');
if (fingerprint !== expectedFingerprint) {
throw new Error('Certificate fingerprint mismatch');
}
},
});
fetch('https://api.example.com', { agent });
// Subresource Integrity (SRI) for CDN resources
<script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>
// Generate SRI hash
const crypto = require('crypto');
const fs = require('fs');
function generateSRI(filePath) {
const content = fs.readFileSync(filePath);
const hash = crypto.createHash('sha384').update(content).digest('base64');
return `sha384-${hash}`;
}
// React component for SRI
function SecureScript({ src, integrity }) {
useEffect(() => {
const script = document.createElement('script');
script.src = src;
script.integrity = integrity;
script.crossOrigin = 'anonymous';
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
};
}, [src, integrity]);
return null;
}
// Nginx SSL configuration
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/certificate.crt;
ssl_certificate_key /path/to/private.key;
# TLS settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers on;
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /path/to/ca.crt;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}
// Let's Encrypt certificate automation
npm install greenlock-express
const greenlock = require('greenlock-express');
greenlock
.init({
packageRoot: __dirname,
configDir: './greenlock.d',
maintainerEmail: 'admin@example.com',
cluster: false,
})
.serve(app);
// Certificate monitoring
const https = require('https');
const tls = require('tls');
function checkCertificate(hostname) {
return new Promise((resolve, reject) => {
const socket = tls.connect(443, hostname, () => {
const cert = socket.getPeerCertificate();
if (!cert || !Object.keys(cert).length) {
reject(new Error('No certificate'));
return;
}
const validTo = new Date(cert.valid_to);
const daysRemaining = Math.floor((validTo - new Date()) / (1000 * 60 * 60 * 24));
resolve({
subject: cert.subject,
issuer: cert.issuer,
validFrom: cert.valid_from,
validTo: cert.valid_to,
daysRemaining,
});
socket.end();
});
socket.on('error', reject);
});
}
// Usage
checkCertificate('example.com').then(cert => {
console.log(`Certificate expires in ${cert.daysRemaining} days`);
if (cert.daysRemaining < 30) {
console.warn('Certificate expiring soon!');
// Send alert
}
});
Frontend Security Best Practices Summary
- XSS Prevention - Use DOMPurify for sanitization, avoid innerHTML/eval/document.write, implement CSP headers, escape user input automatically (React/Vue)
- CSRF Protection - Set SameSite cookies (Strict/Lax), implement CSRF tokens, validate Origin/Referer headers, use custom headers for AJAX
- Content Security Policy - Define strict CSP directives, use nonces for inline scripts, implement CSP violation reporting, prevent inline script execution
- JWT Security - Store tokens in HttpOnly cookies, implement refresh token rotation, use short-lived access tokens (15min), automatic token refresh mechanism
- Input Validation - Use Joi/Yup/Zod schemas, validate on both client and server, sanitize inputs, use parameterized queries for SQL, prevent injection attacks
- HTTPS & TLS - Enforce HTTPS with HSTS headers, use TLS 1.3, strong cipher suites, certificate monitoring, Subresource Integrity for CDN resources
- Defense in Depth - Layer multiple security controls, use helmet.js for Express, implement rate limiting, monitor security violations, regular security audits