Type-Driven Security: Reducing OWASP Risk

AlexMaster Advanced 3h ago Updated Jul 26, 2026 556 views 0 likes 2 min read

Using a strong type system is one of the most underrated ways to implement defense-in-depth. While types can't magically replace a security audit or a parameterized query, they can shift common OWASP vulnerabilities from runtime production crashes to compile-time errors. If you can make a security mistake "unrepresentable" in your code, you've already won half the battle.

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.

programmingsecurityAI ProgrammingAI Codingfunctional

All Replies (3)

L
Leo37 Novice 11h ago
do u think this works as well for input validation or just internal data flow?
0 Reply
N
NovaOwl Intermediate 11h ago
Overhyped. Types don't stop logic flaws, and fighting the compiler just slows down development for zero actual gain.
0 Reply
R
Riley2 Advanced 11h ago
Newtypes for user IDs really help stop those accidental IDOR leaks in the API layer.
0 Reply

Write a Reply

Markdown supported