Mesrai
Back to blog
// essayTechnical Deep Dive

as Casting User Input Without Validation: How Mesrai Suggests Zod

Real PR catch: branded-type cast without validation. Zod brand schema fix.

Mesrai TeamAugust 11, 20268 min read

An API handler with a branded `Email` type. Author cast `req.body.email as Email`. TypeScript happy. Runtime: any string accepted, including obvious non-emails.

The vulnerable diff

tsapi/contact.ts
// types.ts
type Email = string & { __brand: "Email" };

// api/contact.ts
contactRouter.post("/contact", async (req, res) => {
  // BUG: as bypasses any runtime check
  const email = req.body.email as Email;
  await sendWelcome(email);  // accepts "not an email"
});

What is wrong

Branded (nominal) types in TypeScript are a clever compile-time trick: declare a type that includes an intersection with a unique brand, and the type system treats it as distinct from a plain string. But the brand exists only at compile time — there is nothing at runtime to enforce the brand. `as Email` is a free assertion that any string is an Email. The point of the brand is to force consumers to go through a parser that *does* validate; using `as` defeats the entire pattern.

The attack

Failure path:

textexploit
req.body.email = "not-an-email"
const email = req.body.email as Email;   // accepted by TS
await sendWelcome(email);                // tries to send to "not-an-email"
                                         // SMTP error or silent failure.

Variants include casting unvalidated path strings as `AbsolutePath`, raw integers as `UserId`, etc.

Mesrai's review comment

textreview-comment.txt
mesraipilot · Bot · reviewed 1 min ago

[mesrai] [code-review] [Language] [Type-Hole] [medium]

`as Email` defeats the branded type. The brand exists only at
compile time — the cast lets any string through.

Use Zod's brand for nominal types backed by a runtime parse:

  import { z } from "zod";

  export const EmailSchema = z.string().email().brand<"Email">();
  export type Email = z.infer<typeof EmailSchema>;

  // In the handler:
  const result = EmailSchema.safeParse(req.body.email);
  if (!result.success) {
    return res.status(400).json({ error: "invalid email" });
  }
  await sendWelcome(result.data);

Now `result.data` is typed as Email and the runtime check passed.
Same shape works for AbsolutePath, UserId, PositiveInt, etc.

The fix

tsapi/contact.ts (fixed)
// api/contact.ts — fixed
import { z } from "zod";

const EmailSchema = z.string().email().brand<"Email">();
type Email = z.infer<typeof EmailSchema>;

contactRouter.post("/contact", async (req, res) => {
  const result = EmailSchema.safeParse(req.body.email);
  if (!result.success) {
    return res.status(400).json({ error: "invalid email" });
  }
  await sendWelcome(result.data);
});

The schema validates the runtime shape AND brands the output. The Email type now has a runtime guarantee behind it. Downstream code receiving `Email` can trust the format.

Why human review missed it

Branded types look like a strong nominal-types pattern in TypeScript but the discipline is fragile if `as` is used freely. Mesrai catches every `as BrandedType` cast and recommends parsing through a validator that produces the brand.

Related rules + further reading

Mesrai rule pack: language/no-brand-as-cast — flags `as` casts to branded types without runtime parse.

Zod docs: Brand types.

Common in fintech / payments codebases where nominal types are heavily used.

Takeaway

Brand requires parsing. Cast does not parse. Use Zod's `.brand()` and the runtime check stays attached. Mesrai catches every bare cast.

// try

See it on your next PR.

Free for individuals. Install in two minutes. Mesrai reviews every commit.