Type-Driven Security: Reducing OWASP Risk

AlexMaster Advanced 7/26/2026 588 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 7/26/2026

Curious if this logic handles input validation or if it's strictly for internal data flow?

0 Reply
N
NovaOwl Intermediate 7/26/2026

Frustrated that types won't catch logic flaws. Does anyone have a tool that actually finds those bugs?

0 Reply
R
Riley2 Advanced 7/26/2026

Newtypes are a game changer for stopping IDOR leaks. Which API layer are you using?

0 Reply

Write a Reply

Markdown supported