JSONPはもう卒業!CORSで安全に「お隣さん」とデータをやり取りしよう
皆さん、こんにちは!サイバーセキュリティの世界へようこそ。今回は、Web開発でよく耳にする「JSONP」という技術と、それに代わる「CORS」という仕組みについて、できるだけ分かりやすく、そして実践的に解説していきたいと思います。
「インジェクション攻撃」とか「XSS」とか、なんだか難しそうな言葉が出てきて、ちょっと身構えてしまいますよね? でも大丈夫です! 今回は、皆さんの身近な「家の鍵」や「泥棒」に例えながら、攻撃の仕組みと、どうすれば安全にデータをやり取りできるのかを、一歩ずつ紐解いていきましょう。
なぜ「JSONP」は危なくなってきたのか? ~家の鍵を「誰でも開けられる状態」にしていたら?~
まず、JSONPについてお話ししましょう。これは、昔々、Webサイトが異なるドメイン(例えば、あなたの「マイサイト.com」と、別の会社が提供する「サービスAPI.com」)の間でデータをやり取りするのが難しかった時代に、工夫して作られた技術なんです。
例えるなら…
あなたの家(あなたのWebサイト)に、隣の家(外部のAPI)から「美味しいクッキーのレシピ」をもらいたいとします。でも、直接インターホンで聞いても、セキュリティ上の理由で教えてもらえない。そこで、JSONPはこんな方法をとりました。
- 隣の家: 「レシピを紙に書いて、玄関のドアノブにぶら下げておくよ。誰でも取れるようにね!」(JSONPのレスポンス)
- あなたの家: 「あ、ドアノブにぶら下がってる! よし、もらおう!」(あなたのWebサイトがJSONPのデータを受け取る)
この「ドアノブにぶら下げる」というのが、JSONPの仕組みで、callbackという名前の関数を使って、受け取ったデータをその関数に渡すことで、JavaScriptで処理できるようにしていました。
でも、ここに潜む危険が…
この「誰でも取れるようにドアノブにぶら下げる」という方法、実はすごく危ないんです。もし、悪意のある泥棒(攻撃者)があなたの家の前にいて、ドアノブにぶら下がっているレシピをこっそり盗み見たらどうでしょう?
さらに、泥棒は「レシピ」と書かれた紙に、こっそり「このレシピで作ったクッキーを食べると、あなたの秘密の情報が全部隣の家に送られちゃうよ!」なんて悪戯書きをすることもできてしまうんです。
これが、JSONPが抱える XSS (クロスサイトスクリプティング) のリスクです。攻撃者は、JSONPで送られてくるデータに悪意のあるJavaScriptコードを紛れ込ませて、ユーザーのブラウザ上で実行させることができてしまいます。その結果、ログイン情報や個人情報などが盗まれてしまう可能性があるのです。
現代の「安全な鍵」CORSとは? ~「あなたと、この人だけ」という約束~
そこで登場するのが、現代のWeb開発における「安全な鍵」とも言える CORS (Cross-Origin Resource Sharing) です。
CORSは、先ほどのJSONPのように「誰でも取れるようにドアノブにぶら下げる」のではなく、もっと厳密な「本人確認」と「許可証」のような仕組みを提供します。
例えるなら…
あなたの家(あなたのWebサイト)が、隣の家(外部のAPI)から「美味しいクッキーのレシピ」をもらいたい。
- あなたの家: 「ねぇ、レシピを教えてほしいんだけど、僕(マイサイト.com)だよ。特別に僕にだけ教えてくれる?」
- 隣の家: 「うん、いいよ! 君(マイサイト.com)にはレシピを教えてあげる。でも、他の人(他のWebサイト)には教えないからね!」
この「君(マイサイト.com)には教えてあげる」という約束を、HTTPヘッダーという「特別な封筒」に入れて、やり取りするのがCORSの基本です。
CORSの仕組みを「HTTPヘッダー」で見てみよう
CORSでは、主に以下のHTTPヘッダーが使われます。最初はちょっと専門的に感じるかもしれませんが、一つずつ紐解いていきましょう。
1. Access-Control-Allow-Origin ヘッダー ~「誰に許可するか」という許可証~
これは、APIを提供する側(隣の家)が、「どのWebサイトからのアクセスなら許可するか」を指定するヘッダーです。
- APIサーバー側(隣の家)からの返信:
Access-Control-Allow-Origin: https://your-website.com
これは、「https://your-website.com というURLからのアクセスだけを許可しますよ」という意味になります。もし、このヘッダーが指定されていなければ、原則として他のドメインからのアクセスはブロックされます。
「」ワイルドカードについて(注意が必要!)
時々、Access-Control-Allow-Origin: となっているのを見かけるかもしれません。これは「どのドメインからのアクセスでもOK」という意味ですが、JSONPの「ドアノブにぶら下げる」状態と似てしまうため、公開APIでなければ、基本的には特定のドメインを指定することをおすすめします。
2. Access-Control-Allow-Methods ヘッダー ~「どんなお願い(HTTPメソッド)を許可するか」~
APIに対して、どのようなHTTPメソッド(GET, POST, PUT, DELETEなど)でのアクセスを許可するかを指定します。
- APIサーバー側(隣の家)からの返信:
Access-Control-Allow-Methods: GET, POST, OPTIONS
これは、「GET(レシピを見る)、POST(レシピを更新する)、OPTIONS(どんなお願いができるか事前に確認する)というお願いならOKですよ」という意味になります。
3. Access-Control-Allow-Headers ヘッダー ~「どんなお願い(ヘッダー)を許可するか」~
クライアント側(あなたの家)が、APIにリクエストを送る際に、どんなカスタムヘッダー(APIへの追加情報)を付与することを許可するかを指定します。
- APIサーバー側(隣の家)からの返信:
Access-Control-Allow-Headers: Content-Type, Authorization
これは、「Content-Type(データの形式)や Authorization(認証情報)といった追加情報付きのお願いならOKですよ」という意味になります。
補足: OPTIONS メソッド (プリフライトリクエスト)
CORSでは、実際にデータを送受信する前に、ブラウザが自動的に OPTIONS メソッドで「プリフライトリクエスト」というものをAPIサーバーに送信することがあります。これは、APIサーバーが、これから送られてくるリクエスト(例えばPOSTで、特定のヘッダーを付けて送る)を許可しているかどうかの「事前確認」のようなものです。
- あなたの家: 「これから、クッキーのレシピの更新をお願いしたいんだけど、OK?」 (OPTIONSリクエスト)
- 隣の家: 「うん、OKだよ! そのお願いなら受けられるよ!」 (OPTIONSリクエストへの許可応答)
- あなたの家: 「じゃあ、レシピの更新をお願いします!」 (POSTリクエスト)
このように、プリフライトリクエストで事前に確認することで、不要なリクエストや、許可されていないリクエストを未然に防ぐことができます。
実践!CORSの設定例を見てみよう
APIサーバー側 (Node.js + Express の場合)
ここでは、ExpressというNode.jsのフレームワークを使った簡単な例を見てみましょう。
const express = require(‘express’);
const cors = require(‘cors’); // CORSミドルウェアをインポート
const app = express();
const port = 3000;
// CORSミドルウェアを設定
// ここでは、全てのオリジンからのアクセスを許可していますが、
// 本番環境では、より具体的に許可するオリジンを指定しましょう。
// 例: app.use(cors({ origin: ‘https://your-website.com’ }));
app.use(cors());
// JSON形式のリクエストボディをパースするためのミドルウェア
app.use(express.json());
// サンプルAPIエンドポイント
app.get(‘/api/recipes’, (req, res) => {
const recipes = [
{ id: 1, name: ‘チョコレートクッキー’, ingredients: [‘小麦粉’, ‘砂糖’, ‘チョコレート’] },
{ id: 2, name: ‘バニラケーキ’, ingredients: [‘小麦粉’, ‘卵’, ‘砂糖’, ‘バニラ’] }
];
res.json(recipes); // JSON形式でレシピを返す
});
app.listen(port, () => {
console.log(APIサーバーが http://localhost:${port} で起動しました!);
});
ポイント:
const cors = require('cors');で、便利なCORSミドルウェアを読み込みます。app.use(cors());で、Expressアプリケーション全体にCORSを設定します。origin: 'https://your-website.com'のように、具体的なドメインを指定することで、セキュリティを強化できます。
クライアント側 (JavaScript の fetch API を使う場合)
次に、このAPIを呼び出す側のJavaScriptコードを見てみましょう。
// APIからレシピを取得する関数
async function fetchRecipes() {
const apiUrl = ‘http://localhost:3000/api/recipes’; // APIのURL
try {
const response = await fetch(apiUrl, {
method: ‘GET’, // GETリクエストでデータを取得
headers: {
// 必要に応じて、カスタムヘッダーを追加できます。
// 例: ‘Authorization’: ‘Bearer YOUR_TOKEN’
‘Content-Type’: ‘application/json’ // 送信データがない場合でも、一般的なヘッダーを指定することがあります。
}
});
// レスポンスが成功したかチェック
if (!response.ok) {
throw new Error(HTTPエラーが発生しました: ${response.status});
}
const data = await response.json(); // レスポンスをJSONとしてパース
console.log(‘取得したレシピ:’, data);
// ここで取得したデータを画面に表示するなど、処理を行います。
displayRecipes(data);
} catch (error) {
console.error(‘レシピの取得中にエラーが発生しました:’, error);
// エラーメッセージをユーザーに表示するなどの処理
}
}
// 取得したレシピを表示する簡単な関数 (例)
function displayRecipes(recipes) {
const recipeList = document.getElementById(‘recipe-list’); // HTMLに
があると想定
if (!recipeList) return;
recipes.forEach(recipe => {
const listItem = document.createElement(‘li’);
listItem.textContent = ${recipe.name} - 材料: ${recipe.ingredients.join(', ')};
recipeList.appendChild(listItem);
});
}
// 関数を実行してレシピを取得
fetchRecipes();
ポイント:
fetch(apiUrl, { method: 'GET', ... })で、APIにリクエストを送信します。headersオブジェクトで、必要に応じてContent-TypeやAuthorizationなどのヘッダーを追加できます。response.okで、HTTPステータスコードが成功(200番台)かどうかを確認し、エラーハンドリングを行います。await response.json()で、APIから返されたJSONデータをJavaScriptのオブジェクトとして受け取ります。
Webサーバー側 (Nginx の場合)
もしAPIサーバーがNginxのようなWebサーバーで公開されている場合は、Nginxの設定でCORSヘッダーを付与することもできます。
server {
listen 80;
server_name api.example.com;
location / {
# ここにAPIサーバーへのプロキシ設定などが入ります
# proxy_pass http://your_api_backend;
# CORSヘッダーの設定
add_header ‘Access-Control-Allow-Origin’ ‘https://your-website.com’;
add_header ‘Access-Control-Allow-Methods’ ‘GET, POST, OPTIONS’;
add_header ‘Access-Control-Allow-Headers’ ‘Origin, Content-Type, Accept, Authorization’;
add_header ‘Access-Control-Max-Age’ ‘1728000’; # OPTIONSリクエストの結果をキャッシュする期間(秒)
# OPTIONSメソッドへの対応(プリフライトリクエスト用)
if ($request_method = ‘OPTIONS’) {
add_header ‘Access-Control-Allow-Origin’ ‘https://your-website.com’;
add_header ‘Access-Control-Allow-Methods’ ‘GET, POST, OPTIONS’;
add_header ‘Access-Control-Allow-Headers’ ‘Origin, Content-Type, Accept, Authorization’;
add_header ‘Access-Control-Max-Age’ ‘1728000’;
add_header ‘Content-Length’ ‘0’;
add_header ‘Content-Type’ ‘text/plain charset=UTF-8’;
return 204; # No Content レスポンス
}
}
}
ポイント:
add_headerディレクティブを使って、必要なCORSヘッダーを設定します。OPTIONSメソッドへの対応は、プリフライトリクエストを正しく処理するために重要です。return 204;は、リソースは返さないが、リクエストは成功したことを示すステータスコードです。
まとめ ~安全は「約束」から始まる~
JSONPは、かつては便利な技術でしたが、その仕組み上、XSSのリスクを内包していました。まるで、家の鍵を「誰でも開けられる状態」にしておくようなものです。
一方、CORSは、HTTPヘッダーという「特別な封筒」と「許可証」を使って、どのWebサイトからのアクセスを、どのような方法で許可するかを明確に定義することで、安全なクロスドメイン通信を実現します。これは、「あなたと、この人だけ」という厳密な約束事があるようなものです。
現代のWeb開発では、JSONPを使うのは避け、CORSを適切に設定して、安全なAPI連携を行いましょう。
- APIを提供する側:
Access-Control-Allow-Originなどで、許可するオリジンやメソッドを明確に指定する。 - APIを利用する側:
fetchAPIなどでリクエストを送信する際に、必要なヘッダーを適切に設定する。
最初は少し難しく感じるかもしれませんが、これらの設定は、皆さんのWebアプリケーションを攻撃から守るための、非常に重要な「お約束」です。ぜひ、今回の解説を参考に、ご自身の開発環境でCORSの設定を試してみてください。「一歩ずつ対策を学んでいきましょう!」応援しています!
コメント