Lab 12: Security Types
Overview
Step 1: Branded Types for Injection Prevention
// Prevent SQL injection by making raw strings incompatible with query functions
declare const SqlQueryBrand: unique symbol;
declare const HtmlStringBrand: unique symbol;
declare const UrlPathBrand: unique symbol;
declare const UserInputBrand: unique symbol;
type SqlQuery = string & { readonly [SqlQueryBrand]: typeof SqlQueryBrand };
type HtmlString = string & { readonly [HtmlStringBrand]: typeof HtmlStringBrand };
type UrlPath = string & { readonly [UrlPathBrand]: typeof UrlPathBrand };
type UserInput = string & { readonly [UserInputBrand]: typeof UserInputBrand };
// Only trusted creation functions can make these types
function sql(strings: TemplateStringsArray, ...params: (string | number)[]): SqlQuery {
const paramPlaceholders = params.map(() => '?').join(', ');
return strings.raw.join(paramPlaceholders) as SqlQuery;
}
function sanitizeHtml(raw: string): HtmlString {
const escaped = raw
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
return escaped as HtmlString;
}
function validateUrlPath(path: string): UrlPath {
if (!/^\/[\w/-]*$/.test(path)) throw new Error(`Invalid URL path: ${path}`);
return path as UrlPath;
}
// Functions ONLY accept branded types — raw strings are ERRORS at compile time
function executeQuery(query: SqlQuery): Promise<unknown[]> {
return db.execute(query);
}
function renderHtml(html: HtmlString): void {
element.innerHTML = html; // Safe — we know it's sanitized
}
// Usage:
executeQuery(sql`SELECT * FROM users WHERE id = ${userId}`); // ✓ OK
// executeQuery("SELECT * FROM users"); // ✗ Error: string ≠ SqlQuery
// executeQuery(userInput); // ✗ Error: UserInput ≠ SqlQueryStep 2: Generic Opaque Type
Step 3: Secret — Redact Sensitive Data
Step 4: Type-Safe CSRF Token Flow
Step 5: Compile-Time Permission System
Step 6: Type-Safe API Keys
Step 7: Validation Pipeline with Types
Step 8: Capstone — Branded Type Security
Summary
Pattern
Type
Prevents
Last updated
