本当にあった Web アプリケーションの脆弱性
この記事の目的
今まで Web アプリケーション製作を行った経験が無い方が「ちょっと個人開発で何か作ってみようかな!」と思ったときにうっかり脆弱性を作りこんでしまうことを少しでも防げたらいいなと考えました。
そのためにはまず脆弱性を他人事だと考えないことが大事だと思ったので、私が過去の開発現場において実際に遭遇したことがある Web アプリケーションの脆弱性の事例を幾つか紹介します。
紹介の前に注意喚起をします。
自分が管理しているわけではない Web アプリケーションに対し、依頼されてもいないのに脆弱性を探す行為は絶対にしないでください。その行為の内容によっては犯罪になる可能性もあります。
外部から渡されたデータを何の対策もなしに SQL に埋め込んでいる
SQL インジェクション攻撃の説明でよくみられる例ですが、ログイン処理でユーザーが入力した ID とパスワードに一致するユーザー情報を取得する SQL 文を発行するとします。
この SQL 文を構築するとき、入力された ID とパスワードがそれぞれ $id
, $password
という変数に入っているとして
$sql = "SELECT * FROM user WHERE password = '" + $password + "' AND id = '" + $id + "'";
このように素朴に文字列連結してしまうと脆弱性となります。
この場合、$id
に 1' OR '1' = '1
と入力することで全件ヒットする SQL 文を発行することが可能となります。
私が遭遇した事例は悪用しようとしても悪用はできないだろうなと思うような箇所でしたが、自己判断は危険なので、どんなときでも安全な方法で SQL 文を組み立てるべきです。
対策
外部から入力された値を用いて SQL 文を発行する場合、必ずプレースホルダ機能を利用したり、エスケープ処理を施したりしてください。
フロントエンドに返却するリソースの所有者を確認していない
認可制御の不備事例です。まず
- アプリケーションにログイン後、帳票一覧ページに画面遷移し、画面上にある「○月の報告書」というリンクを押下すると○月の報告書を閲覧できる
という仕様のアプリケーションがあるとします。
そしてその「○月の報告書」の閲覧 URL は
https://example.com/report/11111
のようなもので、末尾の「11111」は表示対象のデータを一意に特定する id です。
私が遭遇した脆弱性は、ログインしたユーザーがこの「11111」を適当な値にしてアクセスしたとき、他人の報告書でも閲覧できてしまうというものでした。
この脆弱性が存在した当時の実装では、
- 未認証のユーザーは報告書を閲覧できない
- 認証済みのユーザーは、URL で指定された報告書が存在すれば閲覧できる
という状態になっていて、報告書の所有者が認証済みのユーザーと一致することを確認していませんでした。
対策
外部から入力された値を使って何らかのリソースを取得する際はログインユーザーを確認し、そのユーザーが取得できるリソースであるかどうかを必ず確認してください。
ユーザーが入力した Web サイトの URL を正しくバリデーションせず、そのまま a タグの href 属性に入れている
Zenn もそうですが、ユーザーが自分のプロフィールに自由に Web サイトの URL を入力できるアプリケーションがあります。
このような入力欄で入力された URL は、大抵は
<a href="https://blog.84b9cb.info">ホームページ</a>
のように a タグの href 属性に入れられると思います。
ところで a タグの href 属性について、MDN によると
ハイパーリンクが指す先の URL です。リンクは HTTP ベースの URL に限定されません。ブラウザーが対応するあらゆるプロトコルを使用することができます。
と記載されており、皆さんもクリックするとメールアプリが起動する mailto:
URL や、タップしたらスマホの電話アプリが起動する tel:
URL を使ったことがあるかもしれません。
そして href 属性には JavaScript を記述することができます。たとえば
<a href="javascript:alert('test!');">ホームページ</a>
という記述は正しくて、クリックしたら "test!" というダイアログが表示されます。
つまり、HTTP ベースの URL を入力することを想定した入力欄においてバリデーションが甘い場合、攻撃者が任意のスクリプトを挿入することを許す脆弱性となり得ます。
対策
Web サイトの URL を入力することを想定した入力欄では、URL 形式であるだけではなく、その URL のプロトコルも確認してください。
ライブラリを使ってバリデーションしている場合もあると思いますが、たとえば Zod の z.string().url()
は URL 形式であることしか確認しないので、別途プロトコルチェックが必要です。
問い合わせフォームで入力されたメールアドレスに入力された問い合わせ内容をそのまま記載して送っている
一般的な問い合わせフォームで、
- 名前
- メールアドレス
- 問い合わせ内容
という入力項目があり、フォーム送信後に「問い合わせを受け付けました」という自動返信メールを返す仕様はよくあると思います。
このアプリケーションでは自動返信メールに問い合わせ内容を丸ごと記載していたとします。
さて、悪意ある人間が、何らかの手段で入手した他人のメールアドレスを入力し、問い合わせ内容にフィッシングサイトの URL を入力して問い合わせフォームを送信したらどうなるでしょうか。
あなたのメールサーバーからフィッシング詐欺メールが送信されることになります。
※余談ですが、これは私がかつて独学で Laravel を勉強し、「問い合わせフォームできた〜!」と無邪気に個人サイトに設置したときのアプリケーション仕様そのものです。(幸い悪用はされませんでした。)
対策
最も確実なのは自動返信をしないか、自動返信メールに問い合わせ内容を含めないことです。
絶対に問い合わせ内容を自動返信メールに含めたい場合はこれといった決め手となる対策が無いように思います。
reCAPTCHA などを導入して bot を弾くとか、フィッシングサイトの URL が自動返信メールに記載されないように検査するなどの工夫をする必要があると思います。
パスワードを忘れたユーザーに対しパスワードそのものを記載したメールを送信している
これだけは私が開発者として関わったアプリケーションではなく、ユーザーとして利用したことがあるアプリケーションの話です。
パスワードを忘れたユーザーに対し、パスワードを記載したメールを送信するアプリケーションが存在しました。
パスワードそのものをメールに記載できるということは、そのアプリケーションは少なくともパスワードをハッシュ化していないことになります。
暗号化はしているかもしれませんが、暗号化では復号鍵を知っている内部犯による犯行を防げません。
営業秘密情報を転職先に持ち出す犯罪事例など、内部犯による情報漏洩は決して少なくありません。
よって暗号化は十分な対策とは言えないと思います。
対策
パスワードは十分な強度を持つハッシュ関数でハッシュ化しましょう。
パスワードを忘れたユーザーに対しては、パスワードを再設定できるページを作ってパスワードを再設定してもらいましょう。
以前は、ユーザーに入力されたメールアドレスに、パスワード再設定画面の URL を直接記載したメールを送信する仕様が多かったように思います。
しかし最近ではワンタイムパスワードをメールに記載し、ワンタイムパスワードを入力するとパスワード再設定画面に進めるという仕様も増えてきたと感じます。
「メールに記載された URL をクリックする」という行為にユーザーを慣れさせることが、フィッシングサイトの URL を安易にクリックしてしまうことに繋がっているかもしれないという懸念もありますし、今後はワンタイムパスワード方式のほうが良いのかもしれません。
終わりに
実際に遭遇したことがある脆弱性を幾つかご紹介しました。
実体験としてプログラミング歴の長さによらず脆弱性を作りこんでしまうことはありますし、レビューしてもらってもすり抜けてリリースされることがあります。
自分は大丈夫だと思わず気を付けてください。
ちょっと株式会社(chot-inc.com)のエンジニアブログです。 フロントエンドエンジニア募集中! カジュアル面接申し込みはこちらから chot-inc.com/recruit/iuj62owig
Discussion