【TypeScript】Branded typeでセキュリティを強化する

こんにちは、フリーランスエンジニアの太田雅昭です。

以前にzodでのbrandについての記事を書きました。

しかしDrizzleなどを使用していると、直接zodに変換できなかったりするため、TypeScriptプレーンでbrandを行う方法です。

Branded type

Branded typeは、通常の型の中でさらに区別をつけるためのものです。たとえば

// 普通
type Name = string;

// Branded
type CatName = string & {__catname: true}

こうすることで、CatNameはただのstringではなく、特別なstringということになります。

symbolを使う

上記方法でもいいのですが、unique symbolを使うと一意性が保たれてより良いです。

type CatName = string & {readonly __brand: unique symbol};

さらに変換関数もつけると、便利になります。

export function brandCatName(name: string){
  return name as CatName
}

ただ、単にasで変換しているだけなので、あまり意味は見出せません。

セキュリティ強化に使用する

ユーザーのパスワードの漏洩を防止する目的で考えてみます。

type User = {
  name: string;
  password: string;
}

今時は、生でパスワードを保存することはありません。適切なアルゴリズムのライブラリを用いてハッシュ化したものが保存されます。そのため漏れてもすぐに危険ということはないのですが、ローカル保存されると高速でアタックされて破られる可能性も出てくるため、ハッシュといえど漏れないようにするのが基本です。また気づかずにクライアントに送信している場合もあるため、サニタイズは必ずサーバー側で行う必要があります。

ここでパスワードなしの型を作ってみます。

type UserSanitized = Omit<User, 'password'>

これを使って、下記のような処理を入れます。

function showUser(user: UserSanitized){
  alert(JSON.stringify(user));
}

さてここで、UserSanitizedを受け取っているため、一見安全に見えます。しかし実際にはパスワードが入ってきていても、型エラーにはなりません。

showUser({name: 'Taro', password: 'abc'}) // 型エラーにならない

これは問題です。

そのため、UserSanitizedをbrand typeにし、変換関数でサニタイズします。これはサーバー側で行う必要があります。

export type UserSanitized = Omit<User, 'password'> & {readonly __brand: unique symbol}

export function sanitizeUser(user: User){
  const {password, ...omitted} = user;
  return omitted as UserSanitized;
}

なおサーバー側であることを明確にするため、下記のような関数を使用するのも有効です。

function ensureServer() {
  if (typeof window !== 'undefined') {
    throw new Error('This function should be called on the server');
  }
}

sanitizeUserを使用することで、下記のようになります。簡略化して書いています。

const user: User = {name: 'Taro', password: 'abc'};

function showUser(user: UserSanitized){
  alert(JSON.stringify(user));
}

function main(){
  showUser(user); // 型エラーになる

  // 実際はサーバー側で行う
  const sanitized = sanitizeUser(user);
  showUser(sanitized); // 通るようになる
}

これで、sanitize関数を必ず通す必要が出るため、セキュリティの向上が計れます。可能なら、User type自体をexportせずに、SanitizedUserだけexportできるとなお堅牢です。

注意

brand typeはasをすると突破されてしまいます。

showUser(user as UserSanitized); // 通ってしまう

ここは注意ですね。