こんな脆弱性診断レポートはいやだ!

■ Webアプリ脆弱性診断レポートに書かれたくない「ちょっと残念なWebアプリ」たち ■

初めましてこんにちはー
弥生でアプリケーションセキュリティを担当している大谷です。

約10年前の育休中に一念発起して、セキュリティエンジニアを目指すぞー!と、育休しながら勉強して(ある意味リスキリング?)、これまで、Webアプリのセキュリティテストをメインにそこそこやってきました。

⭐️

弥生にくる前、開発現場に身を寄せつつ SASTとDASTに明け暮れていた中 特に初期開発のWebアプリでは、ほぼ必ずと言っていいほど出てくる “指摘” たちがいました。

そういうのを見るたびに「あーまた君たちか……」と デベロッパ向けのメッセージに ペタっと定型文を貼り付ける。。。

⭐️

明確な脆弱性とまでは言えなくても、見る人が見れば「このWebアプリ、あんまセキュリティ考えてないな」とすぐ見破られてしまうよ? (攻撃対象としてマークされるリスクもあるかも!?)

「付けたほうがいい」と言われているのに、なぜに付いていない? 「やっているつもり」でも、実は効いていないよ?

設計でも実装でもDASTでも潰せずに(もしくはやってなくて)、最後の砦:第三者のプロによる脆弱性診断レポートに(Low や Info が大量に……!)書かれて返ってくると、ちょっと恥ずかしくなっちゃうよ?

⭐️

そんな「ちょっと残念なWebアプリ」にならないように セキュリティの視点で “これは信頼されないかも” と思われがちな実装を実例ベースで紹介し、DevSecOpsの実践の中で自然に組み込めるような視点で、設計・実装・レビューに活かせるチェックポイントを整理してみました!

⭐️

⭐️


Case.01:
クリックジャッキング対策がされていない(X-Frame-Options が未設定)

iframeで自由に埋め込めてしまうWebページは、外部サイトからの悪用リスクがあります。

残念ポイント:

  • iframeによる埋め込みを許可すると、見た目は同じだが中身が別物という状況が作れてしまい、ユーザーを騙す攻撃(クリックジャッキング)が可能になります。

対策:

X-Frame-Options: DENY

あるいは

X-Frame-Options: SAMEORIGIN

を明示的に設定することで、意図しない場所で自分のアプリが表示されることを防止できます。

⭐️


Case.02:
CSPが存在しない(そして誰も制御していなかった)

Content-Security-Policy: (なし)

残念ポイント:

  • XSSやクリックジャッキングなど ブラウザ側で防げる攻撃 に対して無防備
  • セキュリティヘッダとして当たり前のように求められているのに、完全に抜けている
  • プロダクトとして「セキュリティ意識が低い」と判断されやすい
  • 「なんとなくまだ必要性が分からない」「動かなくなるのが怖いからとりあえず外しておこう」という状態で本番に出てしまっているケースが多い

対策:

  • 最低限でも以下を設定しておくと「意識してる感」は出ます
Content-Security-Policy: default-src 'self'; script-src 'self'
  • inline script を避けられない場合は unsafe-inline ではなく nonce を導入することを検討
  • フレーム埋め込みやフォーム送信の制御も frame-ancestors, form-action などでCSPに明示的に記述できる

CSPが無いことは「ドアに鍵をかけていない」ようなものです。

攻撃される前に、“鍵かけてますよ”という意思表示をしておきましょう。

⭐️


Case.03:
CSPとDOM操作がかみ合ってない、unsafe-inline が本当に必要?

Content-Security-Policy: script-src 'self' 'unsafe-inline'

こういうCSPを見かけると、「CSPついてるっぽいけど、それって意味あるの?」と思ってしまいます。

残念ポイント:

  • unsafe-inline があると、XSSを防げない
  • 一方で、DOM操作側が innerHTMLdocument.write を使っていて、CSPに頼るしかない構造になっていると、脆弱性リスクが高まる
  • 結果、「攻撃はできるが、CSPが許している」という構図になりやすい

よくある危ないDOM操作パターン:

const msg = new URLSearchParams(location.search).get("msg");
document.getElementById("output").innerHTML = msg;

または:

document.write("<div>" + location.hash + "</div>");

ユーザ操作やURLに含まれる値を、HTMLとしてそのまま出力する処理は、CSPが甘い場合に即XSSに直結します。


unsafe-inline をどうしても使う場合に気をつけたいこと:

  1. DOM操作でHTMLを直接出力しない
    • innerHTMLtextContent に切り替える
    • document.write() は基本的に使用禁止とする
  2. スクリプトを構造的に記述する
    • eval()Function() コンストラクタを使わない
    • setTimeout("コード文字列", 0) ではなく、関数参照を渡す
  3. 可能であれば nonce ベースのCSP に移行を検討
    • script-src 'self' 'nonce-abc123' により、明示的に許可されたスクリプトだけを実行

最終的には:

unsafe-inline を付けるということは、「スクリプトの実行に責任を持ちます」と宣言しているのと同じ。

その責任を果たすには、JavaScriptの書き方にも相応の配慮が必要です。

⭐️


Case.04:
HTTPSを使っているのに、HSTSが設定されていない

HTTPSが有効になっているだけでは、安全な通信は保証されません。

残念ポイント:

  • ユーザーが http://〜 でアクセスしてしまった場合に、自動でHTTPSにリダイレクトされなければ、SSL Stripなどの攻撃に巻き込まれる可能性があります。
  • 自動でHTTPSにリダイレクトしていたとしても、最初の http:// アクセスが暗号化されていない時点で、攻撃者に改ざん・介入される余地が残ってしまいます。

対策:

Strict-Transport-Security: max-age=31536000; includeSubDomains

本当に信用されたいなら、HSTSプリロード対応も検討しましょう。

⭐️


Case.05:
セッションCookieに必要な属性が足りない(Secure, HttpOnly, SameSite など)

Set-Cookie: session_id=abc123; Path=/;

こんなCookieヘッダを見かけると「これは信用していいのか…?」と不安になります。

残念ポイント:

  • Secure がない → HTTPS通信中でもCookieが平文で送信されるリスク
  • HttpOnly がない → XSSがあった場合、JavaScriptから盗まれるリスク
  • SameSite がない → CSRF攻撃に対して無防備
  • Max-Age が長すぎる → セッションハイジャック時の影響が長く続く

対策例:

Set-Cookie: session_id=abc123; Path=/; Secure; HttpOnly; SameSite=Lax

必要に応じて SameSite=StrictMax-Age の調整も忘れずに。

⭐️


Case.06:
nosniffなし?それファイル扱ってるアプリとしてどうなの

X-Content-Type-Options: nosniff

この1行がないことで、予期しない挙動がブラウザ側で勝手に発生する可能性があります。

残念ポイント:

  • サーバが Content-Type: text/plain を返していても、実際の中身がHTMLっぽいとブラウザが勝手にHTMLとして解釈し、スクリプトが実行されることがある(MIMEスニッフィング)
  • 攻撃者が .txt.csv に偽装した悪意あるファイルをアップロードし、スクリプト実行まで持ち込むことも可能に

対策:

X-Content-Type-Options: nosniff

これをレスポンスにつけるだけで、ブラウザに「お前の判断じゃなく、指定されたContent-Type通りに処理しろ」と伝えることができます。

⭐️


Case.07:
レスポンスに適切なキャッシュ制御がされていない

Cache-Control: public

これ、ログイン後の画面や個人情報の表示ページに出てくると、けっこう冷や汗モノです。

残念ポイント:

  • センシティブな情報を含むページがブラウザやプロキシキャッシュに保存されるリスク
  • 家族や同僚と共有しているPCなどで、他の人のアカウント情報が再表示されてしまうリスクもあります。

フレームワークの罠:

  • フレームワークやミドルウェアの設定次第では、Cache-Control: public, no-cache のような意図しないキャッシュ制御が行われている場合もあります。
  • これは「キャッシュは許可するけど毎回再検証する」という意図ですが、プロキシや中間キャッシュに内容が残る可能性があるため、安全とは言えません
  • 代表的なフレームワークであっても、明示的に設定しないと意図しないキャッシュ制御がなされることがあるため注意が必要です。

対策:

Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache

個人情報を含む画面では原則キャッシュ禁止が基本。

「ログアウトしたのに戻るボタンで見えちゃう」などのトラブル、これで防げます!

⭐️


Case.08:
jQueryなどのライブラリがちょっと古すぎる

<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>

このバージョン、リリースは2019年。ぱっと見まだ使えそうに見えるけれど、実は既知の脆弱性が放置されたままです。

残念ポイント:

  • jQuery 3.5.0 より前のバージョンには、HTMLの扱いに関するXSS脆弱性(CVE-2020-11022 など)が存在
  • セキュリティ対応されている最新のLTSバージョンとの差があると、「更新されてないな」という印象を与える
  • 診断レポートで「既知の脆弱性のあるライブラリが使われています」と指摘されると、チーム内でもちょっと気まずくなるやつ

対策:

  • jQueryを使っている場合は、最低でも3.5.0以降(可能なら最新版)へのアップデートを検討
  • package.json や CDNリンクなど、アプリに含まれる依存のバージョンを棚卸しする機会を定期的に設ける
  • フロントエンドでも npm audityarn audit をCIに組み込むと◎

「“今も動いてるからいいでしょ?”ではなく、“そのバージョンでいい理由があるか?”が大事です」

⭐️


おわりに

こうした“ちょっとした設定漏れ”や“つもり実装”が、Webアプリの信頼性をじわじわと下げていきます。信頼できるWebアプリとは、「脅威がないこと」ではなく、「脅威に備えていること」が明示されているアプリ。

この記事が、設計レビューやコードレビュー、そしてDevSecOpsの一環としてのチーム共有のきっかけになれば幸いです。

こうしたチェック項目をどう共有し、どうやって自動化していくか(DASTツールの活用)も、今後ご紹介できればと思います。(未定・・)

ここまで読んでくださってありがとうございました!

 

弥生では一緒に働く仲間を募集しています。
www.yayoi-kk.co.jp
弥生のエンジニアに関するnote記事もご覧ください。
note.yayoi-kk.co.jp