ネットワークスイッチに接続された青いイーサネットケーブル
ニュース深掘り

Formbricks の正規表現バリデーション欠陥が示す「スキーマ検証の設計原則」

目次を見る

文字列として保存された正規表現パターンが、本番環境で初めて SyntaxError を投げる。Formbricks というオープンソースのアンケートプラットフォームで発見されたこのバグは、「入力値の形式チェック」と「入力値の意味チェック」を混同したときに起きる典型的な失敗を示しています。

Formbricks は Next.js・TypeScript・React・Tailwind CSS で構築されたアンケート管理プラットフォームで、GitHub スター数は 12,000 を超えます。Qualtrics(商用のアンケートソリューション)の代替として使われることも多く、自社サーバーへのセルフホストも可能です。アンケートの回答欄に正規表現によるバリデーションルールを設定できる機能を持っており、今回の問題はその実装に潜んでいました。

バグの構造:「空でない文字列」と「有効な正規表現」は別物

Formbricks は入力スキーマの定義に Zod(TypeScript 向けのスキーマ宣言・バリデーションライブラリ)を使っています。問題のコードは次のような形でした。

export const ZValidationRuleParamsPattern = z.object({
  pattern: z.string().min(1),
  flags: z.string().optional(),
});

z.string().min(1) は「1 文字以上の文字列であること」だけを確認します。[invalid のような文字列も、文字が入っているという意味では条件を満たしてしまいます。この値はそのままデータベースに保存され、後になって実ユーザーが回答を送信したタイミングで new RegExp("[invalid") が実行されます。JavaScript はここで SyntaxError を投げ、アンケートがクラッシュします。

エラーが発生するのはアンケート作成時ではなく、実ユーザーの操作中です。開発者の手元では再現しにくく、エラーは「サイレント」に本番環境へ届きます。Sentry(エラー監視サービス)でこのクラッシュを再現したところ、SyntaxError: Invalid regular expression: /[invalid/: Unterminated character class というエラーが index.js の 13 行目を指す形で記録されました。

修正の要点:try/catch を Zod の .refine() に組み込む

このバグの修正は、スキーマレベルで正規表現の構文チェックを行う方法を採っています。

const isValidRegexPattern = (pattern: string): boolean => {
  try {
    new RegExp(pattern);
    return true;
  } catch {
    return false;
  }
};

export const ZValidationRuleParamsPattern = z.object({
  pattern: z.string().min(1).refine(isValidRegexPattern, {
    message: "Invalid regular expression pattern",
  }),
  flags: z.string().optional().refine(isValidRegexFlags, {
    message: "Invalid regular expression flags",
  }),
});

.refine()(Zod が提供する「カスタム検証を追加するメソッド」)に isValidRegexPattern を渡すことで、スキーマが値を受け付ける前に RegExp の構文エラーをキャッチできます。正規表現パターンだけでなく、flags フィールドも同様に検証している点が重要です。z のような無効なフラグも SyntaxError を引き起こすため、両方を個別に検証する必要があります。

「どこで検証するか」という設計の判断

フロントエンドの UI 層でエラーを表示することも技術的には可能です。しかしフォームの UI バリデーションは、API を直接叩くことで回避できます。スキーマレベルでの検証は、UI を経由しないリクエストにも適用されるため、より堅牢な防御線になります。

この考え方は「Defense in Depth(多層防御)」と呼ばれる設計原則と重なります。UI 層・API 層・スキーマ層・データベース層のそれぞれで適切な検証を行い、どこか一箇所が抜けても全体が崩れないようにする発想です。Zod を使ったスキーマ定義はその中でも「API と DB の境界」に位置しており、最も確実に機能する検証ポイントです。

日本の開発現場では Yup や Valibot が Zod の代替として使われることもあります。いずれのライブラリでも「文字列の形式チェック」と「意味的な正しさのチェック」を分けて定義できるカスタムバリデーションの仕組みを持っており、同様のアプローチが取れます。

今回のケースで押さえておきたい点を整理すると次のようになります。

  • 文字列として保存されるコードや式(正規表現・SQL テンプレート・JSONPath など)は、保存前に「実行可能かどうか」を検証する必要がある
  • try/catch で実際にパースを試みるのは、構文の妥当性を確認する最も確実な方法の一つ
  • Zod の .refine() のようなカスタム検証メソッドは、こうした「意味的な正しさ」を表現するために設計されている
  • エラー検知ツール(Sentry など)を使ってバグを再現・記録することで、修正の必要性を客観的に証明できる

スキーマ設計で「形式が正しい」と「動作として正しい」を区別する意識を持つことが、このクラスのバグを根本から防ぐ手段です。入力値を後から実行するコードとして扱う箇所では、保存の前段階で「実際に実行してみる」検証を挟むことを検討する価値があります。

参考

I Found a Silent Bug in Formbricks That Crashes Live Surveys at Runtime

この記事について: 本記事は AI を活用して作成し、forva AI 編集部が内容を確認・監修しています。

AI 駆動開発のご相談は forva AI へ。まずはお気軽にどうぞ。