Type-Driven Security: Reducing OWASP Risk
Preventing Data Leaks with DTOs
One of the most common mistakes is returning a database model directly to the API client. This is a disaster waiting to happen—one new sensitive column in your schema and it's suddenly leaked to the frontend. Instead of relying on developers to remember to delete fields, use a strict Data Transfer Object (DTO) pattern.
type UserRow = {
id: string;
email: string;
passwordHash: string;
mfaSecret: string | null;
internalNotes: string | null;
};
type PublicUserDto = {
id: string;
email: string;
};
function toPublicUser(user: UserRow): PublicUserDto {
return { id: user.id, email: user.email };
}
// Always map to the DTO before sending the response
// res.json(toPublicUser(user));By using an allow-list approach, you ensure that adding a field to the database doesn't automatically expose it to the public.
Eliminating ID Confusion with Branded Types
In large systems, everything is a string. It's incredibly easy to accidentally pass a tenantId into a function expecting a userId, which can lead to serious authorization bugs. I use branded types in TypeScript to make these identifiers distinct.
declare const tenantBrand: unique symbol;
declare const userBrand: unique symbol;
type TenantId = string & { readonly [tenantBrand]: true };
type UserId = string & { readonly [userBrand]: true };
function loadUser(tenantId: TenantId, userId: UserId) {
// The compiler now prevents swapping these two arguments
}Explicit Authorization Context
Stop passing isAdmin: boolean through five different function layers. It's vague and error-prone. Instead, define the access level as a type. This forces the developer to provide the correct authorization context before a sensitive function can even be invoked.
type Viewer = { kind: "viewer"; userId: UserId };
type ProjectEditor = { kind: "project-editor"; userId: UserId; projectId: string };
type ProjectAccess = Viewer | ProjectEditor;
function renameProject(access: ProjectEditor, name: string) {
// This function physically cannot be called with a 'Viewer' type
}This isn't a replacement for runtime authorization checks—you still need to verify the session and permissions against your DB—but it makes the AI workflow and developer experience much safer by documenting the requirements directly in the function signature.
