TypeScript Security Programming Frontend Type Safety
TypeScript Security Patterns: Secure Coding Practices
TypeScript Security Patterns: Secure Coding Practices
Introduction
TypeScript’s type system can be leveraged to prevent security vulnerabilities and enforce secure coding practices. In this comprehensive guide, we’ll explore security patterns that make your applications more robust and secure.
1. Type-Safe Input Validation
Secure String Types
// ✅ SECURE: Type-safe string validation
type ValidatedString<T extends string> = string & { readonly __brand: T };
type EmailAddress = ValidatedString<'EmailAddress'>;
type UserName = ValidatedString<'UserName'>;
type Password = ValidatedString<'Password'>;
class InputValidator {
static validateEmail(email: string): EmailAddress | null {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (emailRegex.test(email)) {
return email as EmailAddress;
}
return null;
}
static validateUsername(username: string): UserName | null {
const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;
if (usernameRegex.test(username)) {
return username as UserName;
}
return null;
}
static validatePassword(password: string): Password | null {
// At least 8 characters, 1 uppercase, 1 lowercase, 1 number
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$/;
if (passwordRegex.test(password)) {
return password as Password;
}
return null;
}
}
// Usage
const createUser = (email: string, username: string, password: string) => {
const validatedEmail = InputValidator.validateEmail(email);
const validatedUsername = InputValidator.validateUsername(username);
const validatedPassword = InputValidator.validatePassword(password);
if (!validatedEmail || !validatedUsername || !validatedPassword) {
throw new Error('Invalid input data');
}
// TypeScript knows these are validated
return {
email: validatedEmail,
username: validatedUsername,
password: validatedPassword,
};
};
Secure URL Types
// ✅ SECURE: Type-safe URL handling
type SecureURL = string & { readonly __brand: 'SecureURL' };
class URLValidator {
private static allowedProtocols = ['https:', 'http:'] as const;
private static allowedDomains = ['example.com', 'api.example.com'] as const;
static validateURL(url: string): SecureURL | null {
try {
const parsed = new URL(url);
// Check protocol
if (!this.allowedProtocols.includes(parsed.protocol as any)) {
return null;
}
// Check domain
if (!this.allowedDomains.includes(parsed.hostname as any)) {
return null;
}
return url as SecureURL;
} catch {
return null;
}
}
static createSecureURL(base: string, path: string): SecureURL {
const url = `${base}${path}`;
const validated = this.validateURL(url);
if (!validated) {
throw new Error('Invalid URL');
}
return validated;
}
}
// Usage
const fetchSecureData = async (url: SecureURL) => {
const response = await fetch(url);
return response.json();
};
const apiURL = URLValidator.createSecureURL('https://api.example.com', '/users');
fetchSecureData(apiURL); // TypeScript ensures this is secure
2. Secure Authentication Types
JWT Token Types
// ✅ SECURE: Type-safe JWT handling
type JWTToken = string & { readonly __brand: 'JWTToken' };
type RefreshToken = string & { readonly __brand: 'RefreshToken' };
interface JWTPayload {
sub: string;
exp: number;
iat: number;
role: 'user' | 'admin';
}
class TokenManager {
private static readonly SECRET_KEY = process.env.JWT_SECRET!;
static createToken(payload: Omit<JWTPayload, 'iat' | 'exp'>): JWTToken {
const now = Math.floor(Date.now() / 1000);
const tokenPayload: JWTPayload = {
...payload,
iat: now,
exp: now + (60 * 60 * 24), // 24 hours
};
// In a real implementation, you'd use a JWT library
return `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.${btoa(JSON.stringify(tokenPayload))}.signature` as JWTToken;
}
static validateToken(token: JWTToken): JWTPayload | null {
try {
// In a real implementation, you'd verify the JWT
const payload = JSON.parse(atob(token.split('.')[1]));
if (payload.exp < Math.floor(Date.now() / 1000)) {
return null; // Token expired
}
return payload;
} catch {
return null;
}
}
static refreshToken(token: JWTToken): JWTToken | null {
const payload = this.validateToken(token);
if (!payload) return null;
return this.createToken({
sub: payload.sub,
role: payload.role,
});
}
}
Secure Session Management
// ✅ SECURE: Type-safe session handling
type SessionID = string & { readonly __brand: 'SessionID' };
type UserID = string & { readonly __brand: 'UserID' };
interface SecureSession {
id: SessionID;
userId: UserID;
createdAt: Date;
expiresAt: Date;
isActive: boolean;
}
class SessionManager {
private sessions = new Map<SessionID, SecureSession>();
createSession(userId: UserID): SessionID {
const sessionId = crypto.randomUUID() as SessionID;
const now = new Date();
const session: SecureSession = {
id: sessionId,
userId,
createdAt: now,
expiresAt: new Date(now.getTime() + 24 * 60 * 60 * 1000), // 24 hours
isActive: true,
};
this.sessions.set(sessionId, session);
return sessionId;
}
validateSession(sessionId: SessionID): SecureSession | null {
const session = this.sessions.get(sessionId);
if (!session || !session.isActive || session.expiresAt < new Date()) {
return null;
}
return session;
}
invalidateSession(sessionId: SessionID): boolean {
const session = this.sessions.get(sessionId);
if (session) {
session.isActive = false;
return true;
}
return false;
}
}
3. SQL Injection Prevention
Type-Safe Query Builders
// ✅ SECURE: Type-safe SQL query building
type SQLString = string & { readonly __brand: 'SQLString' };
class SecureQueryBuilder {
private static sanitizeValue(value: any): string {
if (typeof value === 'string') {
return `'${value.replace(/'/g, "''")}'`;
}
if (typeof value === 'number') {
return value.toString();
}
if (typeof value === 'boolean') {
return value ? '1' : '0';
}
throw new Error('Unsupported value type');
}
static select(table: string, columns: string[] = ['*']): SQLString {
const columnList = columns.join(', ');
return `SELECT ${columnList} FROM ${table}` as SQLString;
}
static where(column: string, operator: '=' | '!=' | '>' | '<' | 'LIKE', value: any): SQLString {
const sanitizedValue = this.sanitizeValue(value);
return `WHERE ${column} ${operator} ${sanitizedValue}` as SQLString;
}
static insert(table: string, data: Record<string, any>): SQLString {
const columns = Object.keys(data);
const values = Object.values(data).map(this.sanitizeValue);
return `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${values.join(', ')})` as SQLString;
}
static update(table: string, data: Record<string, any>): SQLString {
const setClause = Object.entries(data)
.map(([key, value]) => `${key} = ${this.sanitizeValue(value)}`)
.join(', ');
return `UPDATE ${table} SET ${setClause}` as SQLString;
}
}
// Usage
const getUserQuery = SecureQueryBuilder.select('users', ['id', 'name', 'email']);
const userFilterQuery = SecureQueryBuilder.where('email', '=', 'user@example.com');
// TypeScript ensures these are safe
const finalQuery = `${getUserQuery} ${userFilterQuery}`;
Parameterized Queries
// ✅ SECURE: Parameterized query types
type ParameterizedQuery<T extends Record<string, any>> = {
sql: string;
params: T;
};
class SecureDatabase {
static createUserQuery(userData: {
name: string;
email: string;
password: string;
}): ParameterizedQuery<typeof userData> {
return {
sql: 'INSERT INTO users (name, email, password) VALUES (?, ?, ?)',
params: userData,
};
}
static getUserByEmailQuery(email: string): ParameterizedQuery<{ email: string }> {
return {
sql: 'SELECT * FROM users WHERE email = ?',
params: { email },
};
}
static updateUserQuery(userId: number, updates: Partial<{
name: string;
email: string;
}>): ParameterizedQuery<{ id: number } & typeof updates> {
const setClause = Object.keys(updates)
.map(key => `${key} = ?`)
.join(', ');
return {
sql: `UPDATE users SET ${setClause} WHERE id = ?`,
params: { id: userId, ...updates },
};
}
}
4. XSS Prevention with Type Safety
Safe HTML Types
// ✅ SECURE: Type-safe HTML handling
type SafeHTML = string & { readonly __brand: 'SafeHTML' };
type UnsafeHTML = string;
class HTMLSanitizer {
private static allowedTags = ['p', 'div', 'span', 'strong', 'em', 'br'] as const;
private static allowedAttributes = ['class', 'id'] as const;
static sanitize(html: UnsafeHTML): SafeHTML {
// In a real implementation, use DOMPurify or similar
const sanitized = html
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
.replace(/javascript:/gi, '')
.replace(/on\w+\s*=/gi, '');
return sanitized as SafeHTML;
}
static createSafeElement(tag: typeof this.allowedTags[number], content: string): SafeHTML {
return `<${tag}>${this.sanitize(content)}</${tag}>` as SafeHTML;
}
static createSafeAttribute(attr: typeof this.allowedAttributes[number], value: string): SafeHTML {
const sanitizedValue = value.replace(/[<>'"]/g, '');
return ` ${attr}="${sanitizedValue}"` as SafeHTML;
}
}
// Usage
const renderUserContent = (userInput: string) => {
const safeContent = HTMLSanitizer.sanitize(userInput);
return HTMLSanitizer.createSafeElement('div', safeContent);
};
Secure Template Literals
// ✅ SECURE: Type-safe template literals
type SafeTemplate = string & { readonly __brand: 'SafeTemplate' };
class TemplateBuilder {
static createUserCard(name: string, email: string): SafeTemplate {
const safeName = HTMLSanitizer.sanitize(name);
const safeEmail = HTMLSanitizer.sanitize(email);
return `
<div class="user-card">
<h3>${safeName}</h3>
<p>${safeEmail}</p>
</div>
` as SafeTemplate;
}
static createAlert(message: string, type: 'success' | 'error' | 'warning'): SafeTemplate {
const safeMessage = HTMLSanitizer.sanitize(message);
return `
<div class="alert alert-${type}">
${safeMessage}
</div>
` as SafeTemplate;
}
}
5. Secure API Types
Type-Safe API Responses
// ✅ SECURE: Type-safe API handling
type APIResponse<T> = {
success: true;
data: T;
} | {
success: false;
error: string;
code: number;
};
interface SecureUser {
id: UserID;
name: string;
email: EmailAddress;
role: 'user' | 'admin';
createdAt: Date;
}
class SecureAPIClient {
private baseURL: SecureURL;
constructor(baseURL: SecureURL) {
this.baseURL = baseURL;
}
async get<T>(endpoint: string): Promise<APIResponse<T>> {
try {
const response = await fetch(`${this.baseURL}${endpoint}`, {
credentials: 'include',
});
if (!response.ok) {
return {
success: false,
error: `HTTP ${response.status}`,
code: response.status,
};
}
const data = await response.json();
return {
success: true,
data,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
code: 500,
};
}
}
async post<T>(endpoint: string, data: any): Promise<APIResponse<T>> {
try {
const response = await fetch(`${this.baseURL}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(data),
});
if (!response.ok) {
return {
success: false,
error: `HTTP ${response.status}`,
code: response.status,
};
}
const responseData = await response.json();
return {
success: true,
data: responseData,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
code: 500,
};
}
}
}
// Usage
const apiClient = new SecureAPIClient(URLValidator.createSecureURL('https://api.example.com', ''));
const getUser = async (userId: UserID): Promise<APIResponse<SecureUser>> => {
return apiClient.get<SecureUser>(`/users/${userId}`);
};
6. Secure Configuration Types
Environment Variable Types
// ✅ SECURE: Type-safe environment configuration
type Environment = 'development' | 'staging' | 'production';
interface SecureConfig {
readonly environment: Environment;
readonly database: {
readonly url: string;
readonly username: string;
readonly password: string;
};
readonly jwt: {
readonly secret: string;
readonly expiresIn: number;
};
readonly cors: {
readonly origin: string[];
readonly credentials: boolean;
};
}
class ConfigManager {
private static validateConfig(config: any): SecureConfig {
const requiredFields = [
'NODE_ENV',
'DATABASE_URL',
'DATABASE_USERNAME',
'DATABASE_PASSWORD',
'JWT_SECRET',
'JWT_EXPIRES_IN',
'CORS_ORIGIN',
];
for (const field of requiredFields) {
if (!process.env[field]) {
throw new Error(`Missing required environment variable: ${field}`);
}
}
return {
environment: process.env.NODE_ENV as Environment,
database: {
url: process.env.DATABASE_URL!,
username: process.env.DATABASE_USERNAME!,
password: process.env.DATABASE_PASSWORD!,
},
jwt: {
secret: process.env.JWT_SECRET!,
expiresIn: parseInt(process.env.JWT_EXPIRES_IN!),
},
cors: {
origin: process.env.CORS_ORIGIN!.split(','),
credentials: true,
},
};
}
static getConfig(): SecureConfig {
return this.validateConfig(process.env);
}
}
7. Security Best Practices Summary
- Use branded types - Create type-safe wrappers for validated data
- Validate all inputs - Implement comprehensive input validation
- Use parameterized queries - Prevent SQL injection attacks
- Sanitize HTML content - Prevent XSS attacks
- Implement secure authentication - Use type-safe token handling
- Validate environment variables - Ensure all required config is present
- Use secure API patterns - Implement type-safe API responses
- Implement proper error handling - Don’t expose sensitive information
- Use readonly properties - Prevent accidental mutations
- Regular security audits - Use TypeScript to catch security issues
Conclusion
TypeScript’s type system provides powerful tools for building secure applications. By leveraging branded types, strict validation, and secure patterns, you can prevent many common security vulnerabilities at compile time.