Lab 12: Security Types

Time: 60 minutes | Level: Architect | Docker: node:20-alpine

Overview

Security engineering with TypeScript's type system: branded types for injection prevention (SqlQuery/HtmlString/UrlPath), Opaque<T,Tag>, Secret<T> that redacts in JSON.stringify, type-safe CSRF flow, and compile-time permission system via type intersections.


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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
  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 ≠ SqlQuery

Step 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

📸 Verified Output:


Summary

Pattern
Type
Prevents

Branded string

SqlQuery, HtmlString

SQL/XSS injection

Opaque ID

Opaque<string,'UserId'>

ID confusion bugs

Secret wrapper

Secret<T>

Accidental logging

CSRF token

Branded string

CSRF attacks

Permission types

Type intersection

Unauthorized access

Validated wrapper

Validated<T>

Unvalidated data persistence

Last updated