security-practices
リポジトリ: open-circle/valibot 分析日: 2026-02-16
概要
valibot はバリデーションライブラリという性質上、セキュリティがライブラリ設計の根幹に組み込まれている。ユーザーが提供する任意の入力データを安全に検証・変換するため、ReDoS 防御、プロトタイプ汚染ガード、リソース枯渇対策が体系的に実装されている。注目に値するのは、これらのセキュリティ対策がコードレビューの人的チェックではなく、ESLint プラグインによる自動検出と、ランタイムのガード関数という二重防御で実現されている点である。
背景にある原則
正規表現は攻撃ベクトルである: バリデーションライブラリでは正規表現がホットパスに位置するため、ReDoS に対して構造的に脆弱になる。人間のレビューではなく静的解析ツール(
eslint-plugin-redos-detector,eslint-plugin-regexp)で機械的に防御すべきである。根拠:library/eslint.config.jsでredos-detector/no-unsafe-regex: errorとregexp/no-super-linear-move: errorを併用している。信頼境界は入口で厳格に、内部は前提を置いて軽量に: スキーマの
~runメソッドは入力をunknownとして受け取り、型チェック後にdataset.typed = trueを設定する。以降のバリデーションアクションはdataset.typedを前提条件として動作するため、型チェックの二重実行を避けながら安全性を維持する。根拠: 全スキーマの~runでtypeofチェック後にtypedフラグを設定するパターン。プロトタイプチェーンは暗黙の攻撃面である: JavaScript のオブジェクト操作(
for...in,in演算子)はプロトタイプチェーンを走査するため、__proto__・prototype・constructorキーが混入するとプロトタイプ汚染が発生する。動的キーを扱う箇所には必ず明示的なガード関数を挟むべきである。根拠:_isValidObjectKeyがrecord,looseObject,objectWithRestの全てで使用されている。DoS 耐性は機能設計に組み込む: セキュリティ対策を後付けするのではなく、機能の振る舞い自体に組み込む。
strictObjectが未知キーの検出を最初の1件で打ち切るのは、「全件報告」という機能要求よりもリソース枯渇防止を優先した設計判断である。根拠:strictObject.ts:219-223のコメント。
実例と分析
1. 三層の正規表現安全性検証
valibot は正規表現の安全性を3つの ESLint プラグインで多層的に検証している。
eslint-plugin-redos-detector は ReDoS 脆弱性を静的解析で検出する。eslint-plugin-regexp は no-super-linear-move(超線形移動によるDoS)と require-unicode-regexp(Unicode モードの強制)を提供する。eslint-plugin-security は推奨設定を適用しつつ、detect-unsafe-regex は偽陽性が多いため無効化し、より精度の高い redos-detector に委譲している。
// library/eslint.config.js:86-98
// RegExp
'regexp/no-super-linear-move': 'error', // Prevent DoS regexps
'regexp/no-control-character': 'error', // Avoid unneeded regexps characters
'regexp/no-octal': 'error', // Avoid unneeded regexps characters
'regexp/no-standalone-backslash': 'error', // Avoid unneeded regexps characters
'regexp/prefer-escape-replacement-dollar-char': 'error', // Avoid unneeded regexps characters
'regexp/prefer-quantifier': 'error', // Avoid unneeded regexps characters
'regexp/hexadecimal-escape': ['error', 'always'], // Avoid unneeded regexps characters
'regexp/sort-alternatives': 'error', // Avoid unneeded regexps characters
'regexp/require-unicode-regexp': 'error', // /u flag is faster and enables regexp strict mode
'regexp/prefer-regexp-exec': 'error', // Enforce that RegExp#exec is used instead of String#match if no global flag is provided, as exec is faster
// Redos detector
'redos-detector/no-unsafe-regex': ['error', { ignoreError: true }], // Prevent DoS regexps偽陽性の抑制は -- false positive コメント付きの明示的な disable で行われている。
// library/src/regex.ts:20-21
// eslint-disable-next-line redos-detector/no-unsafe-regex -- false positive
export const DECIMAL_REGEX: RegExp = /^[+-]?(?:\d*\.)?\d+$/u;ライブラリ全体でこの disable は4箇所のみであり、各箇所に理由が記載されている。
2. 正規表現パターンの一元管理
全てのビルトイン正規表現パターンは library/src/regex.ts に集約されている。各 action(email, uuid, ipv4 等)はこのファイルからインポートして使う。
// library/src/regex.ts:31-32
export const EMAIL_REGEX: RegExp = /^[\w+-]+(?:\.[\w+-]+)*@[\da-z]+(?:[.-][\da-z]+)*\.[a-z]{2,}$/iu;// library/src/actions/email/email.ts:1
import { EMAIL_REGEX } from "../../regex.ts";一元管理の利点: (1) ReDoS 検査対象を一箇所に集中できる、(2) 同じパターンの重複定義を防げる、(3) パターン更新時の影響範囲が明確になる。
3. プロトタイプ汚染ガード
_isValidObjectKey は3つの攻撃ベクトルを同時に遮断する。
// library/src/utils/_isValidObjectKey/_isValidObjectKey.ts:13-19
export function _isValidObjectKey(object: object, key: string): boolean {
return (
Object.hasOwn(object, key)
&& key !== "__proto__"
&& key !== "prototype"
&& key !== "constructor"
);
}この関数は動的キーを扱う全てのオブジェクト系スキーマ(record, looseObject, objectWithRest)で使用されている。テストでは JSON.parse('{"__proto__": 123, "foo": 456}') のような攻撃ペイロードが正しくフィルタされることを検証している。
// library/src/schemas/record/record.test.ts:69-75
test("for record with __proto__ key", () => {
const input = JSON.parse('{"__proto__": 123, "foo": 456}');
expect(schema["~run"]({ value: input }, {})).toStrictEqual({
typed: true,
value: { foo: 456 },
});
});4. strictObject のリソース枯渇防止
strictObject は未知のキーを検出してエラーにするが、意図的に最初の1件で検査ループを打ち切る。
// library/src/schemas/strictObject/strictObject.ts:200-226
for (const key in input) {
if (!(key in this.entries)) {
_addIssue(this, "key", dataset, config, {
input: key,
expected: "never",
path: [
{
type: "object",
origin: "key",
input: input as Record<string, unknown>,
key,
value: input[key],
},
],
});
// Hint: We intentionally break the loop after the first unknown
// entries. Otherwise, attackers could send large objects to
// exhaust device resources. If you want an issue for every
// unknown key, use the `objectWithRest` schema with `never` for
// the `rest` argument.
break;
}
}全件報告が必要なユースケースには objectWithRest(entries, never()) という代替パスを用意し、デフォルトの安全性を犠牲にしない設計としている。
5. object スキーマによる暗黙のキーストリッピング
object スキーマ(strictObject でも looseObject でもないデフォルト)は、定義されたキーのみを出力オブジェクトにコピーする。未知のキーは無視される(エラーにもならない)。これは mass assignment 攻撃に対する防御層として機能する。
// library/src/schemas/object/object.ts:98-107
'~run'(dataset, config) {
const input = dataset.value;
if (input && typeof input === 'object') {
dataset.typed = true;
dataset.value = {}; // 空オブジェクトから再構築
for (const key in this.entries) { // スキーマ定義のキーのみ走査
// ...
}入力オブジェクトを直接渡すのではなく、空オブジェクトに必要なキーだけをコピーする再構築パターンにより、未定義プロパティの漏洩を構造的に防いでいる。
6. セキュリティツール選定のトレードオフ判断
eslint-plugin-security の detect-unsafe-regex を無効化し、redos-detector に置き換えている。
// library/eslint.config.js:119-120
'security/detect-object-injection': 'off', // Too many false positives
'security/detect-unsafe-regex': 'off', // Too many false positives, see https://github.com/eslint-community/eslint-plugin-security/issues/28 - we use the redos-detector plugin instead偽陽性が多いツールを「念のため」有効にするのではなく、より精度の高い代替ツールに置き換えることで、開発者がルールを無視する習慣(アラート疲れ)を防いでいる。
パターンカタログ
Whitelist Pattern (分類: セキュリティ/入力検証)
- 解決する問題: 未知の入力プロパティによる攻撃(mass assignment, prototype pollution)
- 適用条件: 外部入力をオブジェクトとして受け取る場面
- コード例:
library/src/schemas/object/object.ts:107- スキーマ定義のキーのみを走査して再構築 - 注意点:
looseObjectを使うと未知キーが通過するため、セキュリティ要件に応じて使い分ける
Guard Function Pattern (分類: セキュリティ/防御)
- 解決する問題: 危険な操作の散在と検証漏れ
- 適用条件: 同じセキュリティチェックが複数箇所で必要な場面
- コード例:
library/src/utils/_isValidObjectKey/_isValidObjectKey.ts:13- プロトタイプ汚染チェックを1関数に集約 - 注意点: ガード関数を迂回するコードパスが生まれないよう、使用箇所を網羅的にテストする
Good Patterns
- 正規表現の安全性を ESLint で自動検証する: 人間のレビューに頼らず、
eslint-plugin-redos-detectorとeslint-plugin-regexpで ReDoS 脆弱性を CI で自動検出する。偽陽性の disable には必ず理由を記載する。
// library/eslint.config.js:98
'redos-detector/no-unsafe-regex': ['error', { ignoreError: true }],- 正規表現パターンを単一ファイルに集約する:
regex.tsに全パターンを集約し、各 action はインポートして使う。セキュリティ監査の範囲を明確にし、パターンの重複を防止する。
// library/src/regex.ts:4-5
export const BASE64_REGEX: RegExp = /^(?:[\da-z+/]{4})*(?:[\da-z+/]{2}==|[\da-z+/]{3}=)?$/iu;- エラー報告にリソース上限を設ける:
strictObjectの未知キー検出は最初の1件でループを打ち切る。攻撃者が大量のキーを送信してもリソース消費が一定に保たれる。
// library/src/schemas/strictObject/strictObject.ts:219-223
// Hint: We intentionally break the loop after the first unknown
// entries. Otherwise, attackers could send large objects to
// exhaust device resources.
break;- オブジェクト再構築による安全な出力: 入力オブジェクトをそのまま返すのではなく、空オブジェクトに定義済みキーだけをコピーする。未定義プロパティの漏洩を構造的に防止する。
// library/src/schemas/object/object.ts:104-107
dataset.typed = true;
dataset.value = {};
for (const key in this.entries) {Anti-Patterns / 注意点
- 偽陽性の多いセキュリティルールをそのまま有効にする:
eslint-plugin-securityのdetect-unsafe-regexは偽陽性が多く、開発者がアラートを無視する「アラート疲れ」を引き起こす。精度の低いルールは無効化し、より精度の高い代替ツールに置き換えるべき。
// Bad: 偽陽性の多いルールを有効のままにする
pluginSecurity.configs.recommended,
// eslint-disable-next-line security/detect-unsafe-regex // 毎回 disable が必要
// Better: 精度の低いルールを無効化し、専用ツールに委譲する
'security/detect-unsafe-regex': 'off', // redos-detector を代替として使用
'redos-detector/no-unsafe-regex': ['error', { ignoreError: true }],- 動的キーのオブジェクト操作でプロトタイプチェーンを考慮しない:
for...inやkey in objはプロトタイプチェーンを走査するため、JSON.parse等で生成されたオブジェクトの__proto__キーがすり抜ける。
// Bad: hasOwn チェックなしで動的キーを使う
for (const key in input) {
output[key] = input[key]; // __proto__ が混入する可能性
}
// Better: ガード関数で安全なキーのみを通す
for (const key in input) {
if (_isValidObjectKey(input, key)) {
output[key] = input[key];
}
}- バリデーションエラーの全件収集をデフォルトにする: 未知キーや不正入力のエラーを全件収集すると、攻撃者が大量のデータを送信してリソースを枯渇させられる。デフォルトは早期打ち切り、全件収集はオプトインにすべき。
導出ルール
このセクションは必須。最低 3 個のルールを記載すること。synthesis-writer が rules.md 生成時に参照する。 リトマステスト: 「このリポジトリを知らない開発者が読んで、自分のコードに適用できるか?」-- Yes でなければ抽象度が不足している。
[MUST]ユーザー入力を含む正規表現には ReDoS 検出ツール(eslint-plugin-redos-detector等)を CI で適用する- 根拠: valibot は
redos-detector/no-unsafe-regex: errorを設定し、全正規表現を自動検査している(library/eslint.config.js:98)
- 根拠: valibot は
[MUST]動的キーでオブジェクトを操作する際は__proto__・prototype・constructorを明示的にブロックする- 根拠:
_isValidObjectKeyがrecord,looseObject,objectWithRestの全箇所で使用され、テストでJSON.parse('{"__proto__": ...}')の遮断を検証している(library/src/utils/_isValidObjectKey/_isValidObjectKey.ts:13-19)
- 根拠:
[SHOULD]プロジェクト内の正規表現パターンは単一ファイルに集約し、セキュリティ監査の範囲を明確にする- 根拠:
library/src/regex.tsに全パターンを集約し、各 action はインポートで参照している
- 根拠:
[SHOULD]セキュリティ系 ESLint プラグインで偽陽性が多いルールは無効化し、より精度の高い代替ツールに置き換える(アラート疲れの防止)- 根拠:
security/detect-unsafe-regex: offとしredos-detectorに委譲している(library/eslint.config.js:120)
- 根拠:
[SHOULD]バリデーションのエラー報告にはリソース上限を設け、デフォルトで早期打ち切りを行う- 根拠:
strictObjectは未知キー検出を最初の1件でbreakし、コメントで理由を明記している(library/src/schemas/strictObject/strictObject.ts:219-223)
- 根拠:
[SHOULD]外部入力のオブジェクトはそのまま返さず、ホワイトリスト方式で必要なキーのみを新しいオブジェクトにコピーして再構築する- 根拠:
objectスキーマはdataset.value = {}で空オブジェクトを作り、定義済みキーだけをコピーする(library/src/schemas/object/object.ts:104-107)
- 根拠:
[AVOID]偽陽性の多いセキュリティルールを「念のため」有効にしたまま運用すること -- 開発者がルールを無視する習慣が定着する- 根拠: valibot は
security/detect-object-injection: offを明示的に設定し、コメントで理由を記載している(library/eslint.config.js:119)
- 根拠: valibot は
適用チェックリスト
- [ ] プロジェクトに
eslint-plugin-redos-detector(または同等の ReDoS 検出ツール)を導入し、no-unsafe-regex: errorを設定しているか - [ ]
eslint-plugin-regexpのno-super-linear-moveとrequire-unicode-regexpを有効にしているか - [ ] 正規表現パターンが専用ファイルに集約されているか(散在していないか)
- [ ] 動的キーでオブジェクトを操作する全箇所で
__proto__・prototype・constructorをブロックしているか - [ ] オブジェクトバリデーション後の出力は入力オブジェクトの参照ではなく、再構築されたオブジェクトか
- [ ] エラー報告のループにリソース上限(件数制限や早期打ち切り)が設けられているか
- [ ] セキュリティ系 ESLint ルールの偽陽性を
eslint-disableで個別に潰していないか(代替ツールへの置き換えを検討したか) - [ ] セキュリティポリシー(
SECURITY.md)に脆弱性報告の連絡先が記載されているか