こんにちは!ITエンジニアとして日々開発やセキュリティに向き合っている皆さん、お疲れ様です。
システムを作るとき、画面から入ってくる文字や、外部のサービスから送られてくるデータ(APIのペイロード)をしっかりとチェックしていますか?「ちゃんと動くから大丈夫だろう」と油断していると、そこがサイバー攻撃者の格好の侵入経路になってしまうんです。
今回は、新人のIT担当者や「セキュリティって難しそう……」と感じている開発者の皆さんに向けて、APIにおける不正な入力検証(Input Validation)の怖さと、それをピシャリと防ぐJSONスキーマバリデーションの仕組みを、身近な例えを交えながら優しく紐解いていきたいと思います。一歩ずつ、しっかりと対策を学んでいきましょう!
—
1. 家の鍵と泥棒に例える「入力検証」の重要性
まずは、セキュリティの基本を身近な「お家の防犯」に例えて考えてみましょう。
皆さんが暮らす家には、頑丈な玄関のドアや鍵がありますよね。では、そのドアが「外からの泥棒を防ぐためのもの」だとしたら、郵便受けや換気口はどうでしょう? もし、郵便受けから細い棒を突っ込んで内側の鍵を開けられたり、換気口がスカスカで簡単に手が入ったりしたら、どれだけ頑丈な玄関ドアをつけても意味がありませんよね。
APIにおける「入力検証」もこれと全く同じです。
私たちが作るWebアプリケーションやAPIは、いわば「外の世界とつながる玄関や窓」です。そこにやってくるユーザーや外部システムからのデータ(=郵便受けから投げ込まれる荷物)を、「怪しいものが入っていないか」しっかりとチェックせずに家の中へ通してしまうと、悪意ある攻撃者に家の中を荒らされてしまいます。
「うちはパスワードや暗号化(AESやRSAなど)を使っているから安全だ!」と思うかもしれません。しかし、暗号化はあくまで「通信の盗聴や改ざんを防ぐもの」です。届いた中身がそもそも爆弾や毒薬だった場合、それを開封して処理してしまうアプリケーション側が無防備であれば、システムは簡単に乗っ取られてしまいます。だからこそ、届いたデータが正しい形をしているか厳しくチェックする「入力検証」が絶対に必要なんです。
—
2. 攻撃者はどうやって「想定外のデータ」を送り込んでくるのか?
APIがどのように狙われるのか、もう少し具体的に見ていきましょう。
例えば、ユーザー登録用のAPIがあるとします。このAPIは、サーバーに対して以下のようなJSONデータを受け取ることを想定して作られています。
{
"username": "tahakker",
"age": 28
}
開発者は、「名前(文字)と年齢(数字)が送られてくるはずだ」という前提でプログラムを書いています。しかし、攻撃者はこの「想定」の裏をかいてきます。
もし、攻撃者が年齢を入れるべき age という場所に、以下のような悪意あるプログラムの断片(スクリプトなど)を送り込んできたらどうなるでしょうか?
{
"username": "tahakker",
"age": "<script>alert('ハックされました!');</script>"
}
もし、サーバー側が「文字列か数字か」「中身が安全な範囲か」を全くチェックせずにデータベースに保存したり、そのまま画面に表示したりしてしまうと、他のユーザーのブラウザでその悪意あるコードが実行されてしまいます(いわゆるクロスサイトスクリプティングなどの脆弱性につながります)。
また、型を偽装して、システムが予期しない巨大なデータを送り込み、サーバーをメモリ不足でダウンさせる(DoS攻撃)という手口もあります。こうした攻撃を防ぐためには、「届いたデータが、私たちのルールに1ミリも狂いなく一致しているか」を厳格に検査する関所が必要なのです。
—
3. JSONスキーマバリデーションによる厳格な型定義とチェック
そこで登場するのが、今回の主役である「JSONスキーマ(JSON Schema)」です。
JSONスキーマとは、一言で言うと「APIで受け取るデータの設計図・ルールブック」です。「この項目は必ず文字で、長さは3文字以上20文字以内」「この項目は必ず0以上の整数」といったルールを細かく定義し、そのルールに違反しているデータは、家に入る前に玄関先で追い返す(エラーにする)仕組みを作ることができます。
百聞は一見に如かず、実際にNode.js(Express環境など)を想定した具体的なコード例を見てみましょう。
実装例:JSONスキーマを使った安全なAPIの作り方
まずは、必要なライブラリ(ここでは ajv という有名なJSONスキーマバリデーター)を使ったルールの定義とチェックのコードです。
const Ajv = require("ajv");
const ajv = new Ajv(); // バリデーターのインスタンスを作成
// 1. 受け取るデータの「ルールブック(JSONスキーマ)」を定義する
const userSchema = {
type: "object",
properties: {
username: {
type: "string",
minLength: 3, // 3文字以上
maxLength: 20 // 20文字以下
},
age: {
type: "integer",
minimum: 0, // 0歳以上
maximum: 120 // 120歳以下
}
},
required: ["username", "age"], // この2つの項目は必ず存在しなければならない
additionalProperties: false // 定義されていない余計なデータ(不審な荷物)は一切受け付けない
};
// スキーマをバリデーターに登録
const validate = ajv.compile(userSchema);
// 2. APIのエンドポイント(リクエストを受け取る処理)
function handleUserRegistration(reqBody) {
// 3. ルールブックと届いたデータを照らし合わせてチェック!
const valid = validate(reqBody);
if (!valid) {
// ルールに違反している場合(不正な入力の検知)
console.log("バリデーションエラー:", validate.errors);
return {
status: 400,
body: { error: "不正な入力データが含まれています。" }
};
}
// チェックを無事にクリアした安全なデータのみ、以降の処理へ進める
console.log("入力チェックOK!安全なデータです。");
return {
status: 200,
body: { message: "ユーザー登録が成功しました!" }
};
}
// --- テスト実行 ---
// パターンA:正しいデータ
const normalData = { username: "tahakker", age: 30 };
console.log("--- テストA(正常なデータ) ---");
handleUserRegistration(normalData);
// パターンB:不正なデータ(年齢に文字列が入っている、余計なデータがある)
const maliciousData = { username: "badGuy", age: "30歳", isAdmin: true };
console.log("\n--- テストB(不正・不審なデータ) ---");
handleUserRegistration(maliciousData);
このコードのポイントは、additionalProperties: false という設定です。これによって、あらかじめ許可したキー(username, age)以外の余計なデータ(今回のテストBにおける isAdmin など)が混入していた場合、それだけで「不審者!」とみなして弾き返すことができます。攻撃者が隙を突いて裏パラメータを追加しようとしても、この一手で完全にシャットアウトできるわけです。
—
4. 現場のセキュリティ担当者からのアドバイス
実務の現場でAPIを開発する際、入力検証に関してよくある失敗が「フロントエンド(画面側)だけでチェックしているから大丈夫」という思い込みです。
ブラウザやスマートフォンアプリ側で行う入力チェックは、あくまでユーザーの利便性(「入力ミスをその場で教えてあげる親切心」)のためのものです。攻撃者はブラウザをバイパスして、直接APIサーバーに対して怪しいリクエストをいくらでも送り込むことができます。
だからこそ、「すべての入力は悪意あるものと疑え(Trust, but verify)」の原則に従い、必ずサーバー側の入口(APIのコントローラー層)でJSONスキーマなどを用いた厳格なバリデーションを実装してください。
最初はルールの書き方に少し戸惑うかもしれませんが、慣れてしまえば「データの型や仕様が綺麗にドキュメント化される」という副次的メリットも得られ、保守性も劇的に向上します。
「安全なシステムは、堅牢な玄関と、厳格な検問のセットで作られる」。この意識を胸に、今日から皆さんのAPIにも厳格な入力検証を取り入れていきましょう!
コメント