Upgrade to Pro — share decks privately, control downloads, hide ads and more …

はてなサマーインターンシップ2025 セキュリティ 講義資料

Avatar for Hatena Hatena
October 14, 2025

はてなサマーインターンシップ2025 セキュリティ 講義資料

Avatar for Hatena

Hatena

October 14, 2025
Tweet

More Decks by Hatena

Other Decks in Technology

Transcript

  1. ͜ͷߨٛʹ͍ͭͯ • 話すこと • Webアプリケーション開発におけるセキュリティ • アプリケーションエンジニア視点から • ⽬標 •

    セキュリティを気にできるようになる • 「この仕様や実装⼤丈夫か?」と疑問を持てるようになる IBUFOBJOUFSO !
  2. ϢʔβʔͱαʔϏε(ձࣾ)ΛकΔ 例えばサービスの情報が漏えいすると... • ユーザーへの不利益 • 個⼈情報やカード情報の不正利⽤ • プライバシーの侵害 • サービス

    / 会社への不利益 • 経済的な損失 • 信⽤の損失 不利益を⽣まないためにセキュリティが必要 IBUFOBJOUFSO !
  3. ੬ऑੑ = ѱ༻Ͱ͖Δόά • 脆弱性 • 他のユーザーの情報が⾒れる • なりすましが出来る •

    脆弱性を作らないため、ありがちなバグや悪⽤⽅法(攻撃⼿法) を学ぶ必要がある IBUFOBJOUFSO !
  4. ྫ:ϒϩάͷԼॻ͖هࣄҰཡΛฦ͢API app.get('/users/:userId/drafts', (req, res) !" { const userId = req.params.userId;

    !# ύϥϝʔλͷϢʔβʔIDΛऔಘ const drafts = await findDraftsByUserId(userId); !# ϢʔβʔIDͷԼॻ͖هࣄΛऔಘ res.json(drafts); }); • 下書き記事⼀覧を返すAPIの処理 • 下書き記事は、書いたユーザー以外は⾒れない仕様 IBUFOBJOUFSO !"
  5. Ϣʔβʔͷೝূෆඋͷ੬ऑੑ app.get('/users/:userId/drafts', (req, res) !" { const userId = req.params.userId;

    !# ύϥϝʔλͷϢʔβʔIDΛऔಘ const drafts = await findDraftsByUserId(userId); !# ϢʔβʔIDͷԼॻ͖هࣄΛऔಘ res.json(drafts); }); • リクエストがuserIdの下書きを⾒れるかのチェックしていない • userId を知っていれば誰の下書き記事でも取得可能 IBUFOBJOUFSO !!
  6. Ϣʔβʔͷೝূෆඋͷ੬ऑੑ (ରࡦ) app.get('/users/:userId/drafts', (req, res) !" { const loggedInUser =

    !!#; !$ ϩάΠϯϢʔβʔΛऔಘ !$ ϩάΠϯࡁ & userId ͕Ұக͠ͳ͚Ε͹Τϥʔ if (!loggedInUser !% loggedInUser.id !!& req.params.userId) { return res.status(403).send({ error: 'Forbidden' }); } const drafts = db.findDraftsByUserId(req.params.userId); !!# }); • ユーザー認証やパラメータの検証を⾏う IBUFOBJOUFSO !"
  7. Ϣʔβʔͷೝূෆඋͷ੬ऑੑ (ରࡦ) app.get('/users/drafts', (req, res) !" { const loggedInUser =

    !!#; if (!loggedInUser) res.status(403).send({ error: 'Forbidden' }); const drafts = db.findDraftsByUserId(loggedInUser.id); !!# }); • ログインユーザーの記事を返す形にAPIの仕様を変更する⼿もある IBUFOBJOUFSO !"
  8. ྫ:༗ྉهࣄͷൢചػೳ !" Ϣʔβʔ͔ΒૹΒΕͨτʔΫϯ͕༗ޮ͔໰͍߹Θͤ const paymentResult = await paymentGateway.verify(paymentToken); !" ܾࡁ੒ޭͨ͠ͷͰهࣄͷݖརΛߪೖऀʹ෇༩

    if (paymentResult.success) { await db.grantAccessToUser(purchasedUser, articleId); res.status(200).json({ msg: 'Success!' }); } • ユーザーから送られてきたトークンを決済代⾏サービスに問い合せて検証を⾏う • 検証に成功 = 決済完了を確認したので、ユーザーが有料記事を読める処理を実施 IBUFOBJOUFSO !"
  9. ݕূෆඋͷ੬ऑੑ !" Ϣʔβʔ͔ΒૹΒΕͨτʔΫϯ͕༗ޮ͔໰͍߹Θͤ const paymentResult = await paymentGateway.verify(paymentToken); !" ߪೖ੒ޭ͔͠ݟ͍ͯͳ͍!#

    if (paymentResult.success) { await db.grantAccessToUser(purchasedUser, articleId); res.status(200).json({ msg: 'Success!' }); } • 決済の存在と成功しかチェックせず、この記事に対する決済かを確認していない • 別の決済(過去の異なる⾦額の決済等)に差し替えられている可能性がある IBUFOBJOUFSO !"
  10. ݕূෆඋͷ੬ऑੑ (ରࡦ) !" ܾࡁͷଘࡏͱ੒ޭʹՃ͑ͯɺֹ͕ۚਖ਼͍͔͠νΣοΫ if (paymentResult.success !# paymentResult.amount !!$ correctPrice)

    { await db.grantAccessToUser(req.session.user.id, articleId); !" Success !!% } • 決済成功だけでなく、例えば⾦額もチェックする IBUFOBJOUFSO !"
  11. ྫ:Ϣʔβʔ৘ใऔಘAPI !" id, name, email, passwordHash, isAdmin, createdAt ΛؚΜͩΦϒδΣΫτ͕ฦΔ const

    user = db.findUserById(req.params.id); res.json(user); • ユーザー情報をJSONで返すエンドポイント /users/:id • DBから取得した情報をJSON化して返す IBUFOBJOUFSO !"
  12. ػඍ৘ใͷ࿙͍͑ !" id, name, email, passwordHash, isAdmin, createdAt ΛؚΜͩΦϒδΣΫτ͕ฦΔ const

    user = db.findUserById(req.params.id); res.json(user); • 秘匿するべき情報 (passwordHash, isAdmin) まで返している IBUFOBJOUFSO !"
  13. ػඍ৘ใͷ࿙͍͑ (ରࡦ) const user = db.findUserById(req.params.id); const userDto = {

    id: user.id, name: user.name }; res.json(userDto); • DataTransferObjectなどを作成し必要な情報のみ返す IBUFOBJOUFSO !!
  14. SQLΠϯδΣΫγϣϯ SELECT * FROM entry WHERE id = :id; に対して

    !" :id ʹ 1 or 1 = 1ΛೖΕΔͱશͯͷϨίʔυΛฦ͢ SELECT * FROM entry WHERE id = 1 or 1 = 1; !" :id ʹ 1; DROP TABLE entry;ΛೖΕΔͱςʔϒϧ͕ফ͑Δ SELECT * FROM entry WHERE id = 1; DROP TABLE entry; IBUFOBJOUFSO !"
  15. WebΞϓϦέʔγϣϯͰͷ SQLΠϯδΣΫγϣϯ !" desc / asc ΛϑϩϯτΤϯυͰࢦఆ order !# r.URL.Query().Get("order")

    stmt !# fmt.Sprintf("SELECT * FROM entry ORDER BY created_at %s", order) rows, err !# db.Query(stmt) • クエリパラメータの値からソート順を決定する実装 IBUFOBJOUFSO !"
  16. WebΞϓϦέʔγϣϯͰͷ SQLΠϯδΣΫγϣϯ !" desc / asc ΛϑϩϯτΤϯυͰࢦఆ order !# r.URL.Query().Get("order")

    stmt !# fmt.Sprintf("SELECT * FROM entry ORDER BY created_at %s", order) rows, err !# db.Query(stmt) • order に desc; DELETE FROM !!" を⼊⼒されてしまう • ⼊⼒値をそのままSQL⽂の構築に利⽤しているのでSQLi発⽣ IBUFOBJOUFSO !"
  17. SQLΠϯδΣΫγϣϯ (ରࡦ) • プリペアドステートメントを使う • db.Query("SELECT * FROM entry WHERE

    id = ?", order) • ? の部分に⼊る値がエスケープされるので、意図しないSQLが実⾏され ない • 極⼒⾃⾝でSQL⽂を組み⽴てない (⽂字列結合を使わない) • 組み⽴てる場合は⾃前でのエスケープを⾏い、信頼できる値だけにする • ユーザーからの⼊⼒値をそのままSQLに⼊れない IBUFOBJOUFSO !"
  18. SQLΠϯδΣΫγϣϯ (GORMͷྫ) userInput !" "jinzhu;drop table users;" !# safe, will

    be escaped db.Where("name = ?", userInput).First(&user) !# SQL injection db.Where(fmt.Sprintf("name = %v", userInput)).First(&user) • ORM を使っていても発⽣し得る • セキュリティ | GORM IBUFOBJOUFSO !"
  19. ϦμΠϨΫτ !" ?redirect_to=/entry/:id ͳͲ͕ೖΔ const redirectTo = req.query.redirect_to !# '/'

    if (authenticateUser(username, password)) { res.redirect(redirectTo); !" ϩάΠϯ੒ޭޙʹϦμΠϨΫτ͞ΕΔ } • ログイン後に元いたページに戻れるようにする仕様 • クエリパラメータに元いたページを記録し、ログイン成功後そこにリダイレクトする • /entry/:id → /login?redirect_to=/entry/:id IBUFOBJOUFSO !!
  20. ΦʔϓϯϦμΠϨτ੬ऑੑ !" ?redirect_to=https:!"evil.example const redirectTo = req.query.redirect_to !# '/' if

    (authenticateUser(username, password)) { res.redirect(redirectTo); } • redirect_to に任意のURLを指定できる • 外部サイトにリダイレクトされたり、Location ヘッダと通じたXSSに繋がる IBUFOBJOUFSO !"
  21. ྫ:form ཁૉ Λ࢖ͬͨهࣄ౤ߘ <form action="/entry/post" method="POST"> <input type="text" name="title" value="λΠτϧ"

    !" <input type="text" name="body" value="ϒϩάهࣄ" !" !#form> app.post("/entry/post", (req, res) !" { !# ηογϣϯ cookie ͔ΒϢʔβʔΛೝূ !# ͦͷϢʔβʔͷهࣄΛ࡞੒ }); IBUFOBJOUFSO !"
  22. ྫ:form ཁૉ Λ࢖ͬͨهࣄ౤ߘ <form action="https:!"my-site.example/entry/post" method="POST"> <input type="text" name="title" value="εύϜ"

    !# <input type="text" name="body" value="εύϜهࣄ" !# !$form> • form はどのURLに向けてもリクエストを送信できる • これを攻撃者のWebサイトにこれを設置し、ユーザーに送信さ せることでスパム投稿が可能だった IBUFOBJOUFSO !"
  23. CSRF (Cross-site request forgery) • ユーザーの意図に反してリクエストを送信し、Webアプリケー ションの機能を実⾏させる攻撃 • 事例 •

    ⼤量の「はまちちゃん」を⽣み出したCSRFの脆弱性とは? - ITmedia エンタープライズ IBUFOBJOUFSO !"
  24. CSRF τʔΫϯΛຒΊࠐΜͩରࡦ <form action="/entry/post" method="POST"> <input type="hidden" name="csrf" value="${αʔόʔͰܾΊΒΕΔϥϯμϜͳ஋}"> !"form>

    !" τʔΫϯͷ஋͕Ұக͠ͳ͚Ε͹Τϥʔ if (req.body.csrf !!# csrfToken) res.status(403).send({ error: 'Forbidden' }); • 推測できないトークンをformに埋め込み、サーバー側でそのト ークンの検証に成功したときのみ処理を実⾏する • 攻撃者はトークンを推測できないため、CSRF攻撃を防げる IBUFOBJOUFSO !"
  25. CSRF τʔΫϯΛຒΊࠐΜͩରࡦ • 主要なWebフレームワークにはトークンを埋込機能がある • Securing Rails Applications — Ruby

    on Rails Guides • Next.js ではCSRFトークンの機能は無い • How to Think About Security in Next.js | Next.js • この理由を理解するため、オリジンによるブラウザのリソース 保護の仕組みについて話します IBUFOBJOUFSO !"
  26. Same-Origin Policy / Cross-Origin Resource Sharing • Same-Origin Policy (SOP)

    • 他のオリジンにあるリソースへのアクセスを制限するブラウザの仕組み • https:!"my-site.exampleで読み込まれたJSはhttps:!"other-site.exampleのリソースを参照 できない • Cross-Origin Resource Sharing (CORS) • 異なるオリジンのリソースへのアクセスを許可する仕組み • Access-Control-Allow-Originヘッダをサーバーが指定し、リクエストできるオリジンを指定する • これによりhttps:!"my-site.exampleからhttps:!"other-site.exampleのリソースにアクセス 可能 • ただし、単純リクエストの場合はCORSの仕組みは動かない IBUFOBJOUFSO !!
  27. ୯७ϦΫΤετ • 以下の条件のリクエスト • GET / HEAD / POST のいずれか

    • Accept / Accept-Language / Content-Language のヘッダーのいずれか • application/x-www-form-urlencoded / multipart/form-data / text/ plain のContent-Type ヘッダーのいずれか • <form> によって送られるリクエストは単純リクエストなので、CORS関 係なく異なるオリジンに送信できる IBUFOBJOUFSO !"
  28. αΠτ (Site) • URL のスキーム‧eTLD+0(eTLDとその直前にあるドメイン部分)の組み合せ • eTLD = Public Suffix

    List で定義される • hatenablog.com, github.io など • a.hatenablog.com と b.hatenablog.com は同⼀site IBUFOBJOUFSO !"
  29. Cookie ͷ SameSite ଐੑ • Cookieを送信する動作を制御する仕組み • Strict: 同⼀siteで送信される •

    Lax: GETの場合は異なるsiteでも送信される • None: 制限が無い • Chrome/Safari/EdgeはSameSiteがデフォルトでLax IBUFOBJOUFSO !"
  30. SameSite ͱ CSRF • CSRF対策としてある程度有効 • 異なるsiteではCookieが送信されない • https:!"evil.example から

    https:!"my-site.example への<form>リクエスト時に セッションCookieが送信されないので、攻撃不成⽴ • ただし、同⼀サイト内の攻撃は有効なので完全ではない • blog.example が eTLD の場合 • https:!"my-site.blog.exampleで作成されたセッションCookieは、https:!" evil.blog.example から送信出来る • 同⼀サイト内の攻撃は成⽴するので、サーバー側で送信元Originのチェックが合わせて必要 IBUFOBJOUFSO !"
  31. ݱ୅ͷCSRFରࡦ·ͱΊ • Originに基づくリソース保護/SiteによるCookieの送信制限の仕組みがブラウザに存在す る • かといってサーバーサイドで何もしなくてよいわけではない • 送信元Originチェック / SameSite属性の明記

    • Fetch Metadata という仕組みもあるので、合せて確認 • 参考 • 令和時代の API 実装のベースプラクティスと CSRF 対策 | blog.jxck.io • Fetch Metadata でウェブ攻撃からリソースを保護| web.dev IBUFOBJOUFSO !"
  32. iframe • 他のHTMLを埋め込む仕組み • YouTubeの埋め込みなど <iframe width="560" height="315" src="https:!"www.youtube.com/embed/-Q48icZuNhk?si=5OcY8P0z_1MC_qJb&amp;controls=0" title="YouTube

    video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen>!#iframe> IBUFOBJOUFSO !"
  33. iframeͳͲͷຒΊࠐΈΛ੍ݶ͢ΔϔομΛ෇͚ͯରࡦ • Content Security Policy の frame-ancestors • none や

    埋め込み許可リストを設定可能 • X-Frame-Options より柔軟に埋め込み制限をできる • X-Frame-Options • DENY / SAMEORIGIN / ALLOW-FROM origin のみ IBUFOBJOUFSO !!
  34. ྫ:ΫΤϦύϥϝʔλͷ஋Λදࣔ const query = req.query.q !" ''; const html =

    `<h1>'${query}' ͷݕࡧ݁Ռ!#h1>`; res.send(html); IBUFOBJOUFSO !"
  35. ΫΤϦύϥϝʔλͷ஋Λදࣔ const query = req.query.q !" ''; const html =

    `<h1>'${query}' ͷݕࡧ݁Ռ!#h1>`; res.send(html); • クエリパラメータに <script>alert('XSS')!"script> などを⼊れ ることでXSS発動 • https:!#my-site.example/search? q=<script>alert('XSS')!"script> • エスケープやサニタイズを⾏う IBUFOBJOUFSO !"
  36. ΤεέʔϓͱαχλΠζ • エスケープ • HTMLを安全に表⽰するためのエンコードする • < → &lt;, >

    → &gt; • サニタイズ • HTMLを表⽰してもXSSが発⽕しないようにする • DOMPurify を使う IBUFOBJOUFSO !"
  37. ೖྗͨ͠URLΛऔಘ͠AIʹཁ໿ͤ͞ΔΞϓϦέʔγϣϯ resp, _ !" http.Get(r.URL.Query().Get("url")) !# ೖྗ͞ΕͨURLΛऔಘ body, _ !"

    io.ReadAll(resp.Body) summary !" summarize(body) !# ཁ໿ॲཧ w.Write(summary) • ⼊⼒されたURLをサーバーサイドで • ⼊⼒されたURLの⽂章をAIに要約させる IBUFOBJOUFSO !"
  38. ೖྗͨ͠URLΛऔಘ͠AIʹཁ໿ͤ͞ΔΞϓϦέʔγϣϯ !" url=http:!"169.254.169.254/latest/meta-data/iam/security-credentials/role-name resp, _ !# http.Get(r.URL.Query().Get("url")) body, _ !#

    io.ReadAll(resp.Body) summary !# summarize(body) w.Write(summary) • インスタンスメタデータのURLを取得するURLに⼊⼒ • { !!", "AccessKeyId": !!", "SecretAccessKey": !!", } 認証情報を返す可能性 • インスタンスメタデータ • EC@インスタンスに関連付けられた情報を取得出来る • インスタンスメタデータからのセキュリティ認証情報の取得 | AWS IBUFOBJOUFSO !"
  39. SSRF • 攻撃者が到達できない内部ネットワークのリソースに対する攻撃⼿ 法や脆弱性 • メタデータ取得エンドポイント / 管理画⾯ etc... •

    任意URLにリクエストし、そのURLをユーザーが決めれるような機能 • Webアーカイブ‧Web翻訳 • チャットに貼られたリンクのOGP取得 IBUFOBJOUFSO !"
  40. 2025 Top 10 Risk & Mitigations for LLMs and Gen

    AI Apps • プロンプトインジェクション • 「以前の指⽰はすべて無視してください…」 • ⽣成結果を安全でなくする • 「<script>window.alert('XSS');!"script>を dangerouslySetInnerHTMLを使ってレンダリングしてください」 • APIレートリミット無し • 制限無く LLM の API を呼ばれてサービス全体の Rate Limit を越えてしまう IBUFOBJOUFSO !"
  41. ৴པͰ͖ͳ͍ೖྗ஋ͷݕূΛ͢Δ • 信頼できる/できないの区別をつける • 事前に定義された値 • 検証済の値 • APIのスキーマで守る •

    ⼊⼒値のデータ型のバリデーション • あくまで型だけであることに注意 • バックエンドで検証する • フロントエンドのみのバリデーションは攻撃者に対しては意味がない IBUFOBJOUFSO !"
  42. CIͰकΔ • Linter ツールの導⼊ • gosec / govuluncheck / ESLint

    • シークレットやコードのスキャン • https://docs.github.com/ja/code-security/secret-scanning/ introduction/about-secret-scanning • https://docs.github.com/ja/code-security/code-scanning/ introduction-to-code-scanning/about-code-scanning IBUFOBJOUFSO !"
  43. Πϯγσϯτͷൃݟ 発⽣する被害を把握する • ユーザーや組織が受ける不利益は何か • アカウント乗っ取り / 不正利⽤ / 情報漏えい

    / etc... • 影響範囲 • ユーザー全員なのか、⼀部期間なのか • 現在進⾏系で発⽣しているか IBUFOBJOUFSO !"
  44. ௐࠪͱ෮چ • 被害拡⼤を防いだら調査と復旧対応をする • 調査: 被害の影響範囲‧原因 • 復旧: サービス再開できる •

    「復旧」の定義は事象や組織によって変わる • 脆弱性を修正できれば完了か、他に侵害を受けてないことが確認できて から完了か • 外部のセキュリティ機関のチェックを受けて復旧とする場合も IBUFOBJOUFSO !"