【実務・中級編】Service Workerのキャッシュ汚染とセキュリティリスク – アプリケーションセキュリティ & 安全な開発防御ガイド

Service Workerは「最強の脆弱性」にもなり得る:キャッシュ汚染の脅威と実務的防御策

現場でコードをレビューしていると、Service Worker(以下SW)を「オフライン対応のための便利な魔法」程度に考えているエンジニアによく出会う。だが、セキュリティの最前線に立つ我々にとって、SWは「フロントエンドにおけるローカル権限昇格」と同義だ。

一度ブラウザにインストールされたSWは、ユーザーのブラウザ内で永続的に生き続け、HTTPリクエストを横取り(インターセプト)する。もしこれが汚染されたら? ユーザーがアクセスする全てのページに、攻撃者のXSSコードを「ローカルキャッシュから」注入することが可能になる。今回は、この見落とされがちな「SWキャッシュポイズニング」の泥沼と、その回避術を伝授する。

—

1. なぜSWが狙われるのか?(攻撃シナリオ)

攻撃者が狙うのは、アプリケーションの脆弱性だけではない。「CDNやプロキシのキャッシュ」「不正なJavaScriptの挿入」を通じて、SWの登録プロセスを汚染することだ。

もし、攻撃者が何らかの方法でservice-worker.js自体を改ざんできれば、あるいは悪意あるスクリプトでcache.put()を悪用できれば、ブラウザは「正規のサイトから送られてきた」と信じ込んで、攻撃者のペイロードをキャッシュに保持し続ける。

攻撃者による汚染PoC(概念図)

// 悪意のあるスクリプトが混入した場合の例
self.addEventListener(‘fetch’, (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
// 本来のレスポンスに攻撃コードを連結して返す(XSSの永続化)
if (cachedResponse) {
return cachedResponse.text().then(body => {
return new Response(body + ‘‘, {
headers: { ‘Content-Type’: ‘text/html’ }
});
});
}
return fetch(event.request);
})
);
});

一度これがキャッシュされると、ユーザーがブラウザを再起動しようが、正規のコードに修正されようが、ブラウザ内のキャッシュがクリアされるまで攻撃コードが実行され続ける。これが「フロントエンドのバックドア」と呼ばれる所以だ。

—

2. 防御の鉄則:スコープ制限とヘッダー制御

SWを安全に運用するための要件は、以下の3点に集約される。

① Service-Worker-Allowed ヘッダーの厳格管理

SWはデフォルトで「登録元ディレクトリ」以下しか制御できない。これを広げるService-Worker-Allowedヘッダーを不用意に設定してはならない。基本は「パスを限定する」ことだ。

② Nginxによるキャッシュ無効化の設定

SWの本体であるservice-worker.jsをブラウザがキャッシュしてしまうと、修正を反映できなくなる(=脆弱性が修正されない)。以下の設定をnginx.confに記述せよ。

service-worker.js はキャッシュさせない(毎回検証させる)
location /service-worker.js {
add_header Cache-Control “no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0”;
expires off;
proxy_no_cache 1;
proxy_cache_bypass 1;
}

—

3. 実践:セキュアなSW実装テンプレート

安全なSW開発においては、キャッシュのホワイトリスト化と、外部スクリプトの読み込み制限が鍵となる。

// service-worker.js のセキュアな実装例
const CACHE_NAME = ‘v1-safe-cache’;
const ALLOWED_ORIGINS = [‘https://your-app.com’]; // ドメインを厳格に制限

self.addEventListener(‘install’, (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([‘/index.html’, ‘/styles/main.css’]);
})
);
});

self.addEventListener(‘fetch’, (event) => {
const url = new URL(event.request.url);

// 1. オリジン外のリクエストはインターセプトしない
if (!ALLOWED_ORIGINS.includes(url.origin)) return;

event.respondWith(
caches.match(event.request).then((response) => {
// 2. キャッシュがある場合のみ返し、それ以外はfetchする
return response || fetch(event.request).then((networkResponse) => {
// 3. レスポンスの検証(必要に応じてコンテンツセキュリティポリシーを適用)
return networkResponse;
});
})
);
});

—

4. チーフエンジニアからの提言

SWの運用で陥りやすい罠は、「利便性を優先してセキュリティを後回しにする」ことだ。以下のチェックリストをチームの「開発定義書(Definition of Done)」に加えてほしい。

  • スコープの最小化: navigator.serviceWorker.register('/sw.js', { scope: '/app/' }) のように、必要なディレクトリに限定しているか?
  • CSPの併用: Content-Security-Policy ヘッダーで script-src を厳格に管理し、SW経由のXSSすらもブラウザ側で実行させない設計になっているか?
  • キャッシュのライフサイクル: 古いキャッシュを確実に破棄する activate イベントを書いているか?

SWは強力な武器だが、制御を誤れば自らのアプリを攻撃者の踏み台に変えてしまう。「SWは信頼できないリソースである」という前提に立ち、常に最新の検証ロジックを走らせること。それが、現代のWebアプリケーション開発における唯一の正解だ。

何か技術的な壁にぶつかったら、また聞きに来い。現場からは以上だ。

コメント

タイトルとURLをコピーしました