見落としがちなURL正規化によるパストラバーサル
高度解析部アプリケーションセキュリティ課の金子です。
パストラバーサル(またはディレクトリトラバーサル)はXSSやSQLインジェクションに並んでWebアプリケーションに対する代表的な攻撃手法のひとつです。本記事では、パストラバーサルの中でもURL正規化によるパストラバーサルに焦点を当てて攻撃の発生原理やよくある事例について解説します。関連して、PHP向けのAWS SDKで発見したS3バケットに対するパストラバーサルの脆弱性CVE-2023-51651についても紹介します。
2種類のパストラバーサル
パストラバーサルは../
のような文字列を含んだ文字列の正規化処理(normalization)を悪用して、アプリケーションが予期しない"領域"に対してアクセスを行う攻撃です。正規化処理を行う対象によって分類することが可能で、次の2種類のパストラバーサルが代表的です:
- ファイルシステムに対するパストラバーサル:
- ・ 正規化処理の対象: ファイルパス
- URLに対するパストラバーサル:
- ・ 正規化処理の対象: URLパス
パストラバーサルの影響範囲として、ファイルシステム内の任意ファイルへの読み込みや書き込みが紹介されることが多いですが、基本的にはそれは前者のパストラバーサルが前提となっています。パストラバーサル自体の認知度は高いということもあり、XSSやSQLインジェクションの対策が適切に行われているアプリケーションであれば同様に前者のパストラバーサルの対策も適切に行われていることが多い印象があります。一方で、後者のパストラバーサルについては比較的堅牢なアプリケーションにおいても脆弱性として存在していることがあり、見落とされやすいのかもしれません。
URL正規化によるパストラバーサルのよくある発生例
多くのHTTPクライアントではデフォルトでURLの正規化を行う実装になっています:
- Webブラウザの例:
- ブラウザのURLバーに
https://example.com/foo/../
を入力してアクセスすると、実際にはhttps://example.com/
へのHTTPリクエストが送信される。 - JavaScript内で
fetch("https://example.com/foo/../")
を実行すると、実際にはhttps://example.com/
へのHTTPリクエストが送信される。
- ブラウザのURLバーに
- curlの例:
curl "https://example.com/foo/../"
のコマンドを実行すると、実際にはhttps://example.com/
へのHTTPリクエストが送信される。
そのため、HTTPクライアントを介するプログラムにおいて、URLパス部分にユーザの入力値が挿入される状況ではパストラバーサルが発生しやすくなります。
[1] curlでは--path-as-is
オプションを付けることによって、URLパスが正規化されないようになります。また、--request-target /foo/../
のように直接request targetを指定する方法もあります。
[2] 実際にはこのようなケースにおいてはURLのクエリパラメータへのインジェクションも行えることが多いです。また、パストラバーサルはできないが、パラメータインジェクションは可能というケースもしばしばあり、パストラバーサルと同様に適切な対策が求められます。
サーバサイドにおけるパストラバーサル
サードパーティAPIの呼び出しやBFFからバックエンドサーバへのAPI呼び出しなど、サーバサイドで送信されるHTTPリクエストにおいてパストラバーサル脆弱性が存在することがあります。以下はFlaskでのパストラバーサルに脆弱な実装の例です。
from flask import Flask, request
import httpx
API_BASE_URL = "https://api.example.com"
app = Flask(__name__)
api_client = httpx.Client(base_url=API_BASE_URL, auth=("foo", "bar"))
@app.get("/path/to/data")
def get_data():
uuid = request.args.get("uuid", "00000000-0000-0000-0000-000000000000")
return api_client.get(f"/data/{uuid}").json() # path traversal!!!
このとき/path/to/data?uuid=../secret
にアクセスすると、パラメータuuid
の値は../secret
になり、/data/:uuid
ではなく/secret
に対してAPI呼び出しが行われます。
これによって、本来アクセスできないURLパスに対してAPI呼び出しが行うことが可能で、呼び出し先のエンドポイントによってはユーザの権限を超えてデータの取得や書き換え等の攻撃に繋がる可能性があります。上記の例は脆弱性が自明なものではありますが、実際にはユーザの入力値が間接的に混入したり、複数のAPIにまたがって処理が実行されたりして、脆弱性が発見しづらいケースもあります。
クライアントサイドにおけるパストラバーサル
ReactやVue.js等を用いたSPAのアプリケーションでは、以下のフローで処理が実行される実装がよく見られます:
- ① クライアント上でAPIサーバからデータを取得して
- ② 取得したデータに応じてレンダリングを行う
①のステップでパストラバーサル脆弱性が存在することがあり、以下はReact(とreact-router-dom)を用いた脆弱な実装の例です。
[3] 最近ではNext.jsやRemix等のフレームワークの人気もあり、SSRやRSCによってサーバサイドでデータフェッチが行われる場合は、似た実装でサーバサイドにおけるパストラバーサルになることもあると考えられます。
<BrowserRouter>
<Routes>
<Route path="/users/:userId" element={<ProfilePage />} />
</Routes>
</BrowserRouter>
function ProfilePage() {
const { userId } = useParams();
const [user, setUser] = useState({});
useEffect(() => {
// ① クライアント上でAPIサーバからデータを取得
fetch(`/api/users/${userId}`) // path traversal!!!
.then((r) => r.json())
.then((u) => setUser(u))
.catch((e) => console.error(e));
}, []);
// ② 取得したデータに応じてレンダリング
return (
<>
<h1>Profile</h1>
<p>id: {user.id}</p>
<p>name: {user.name}</p>
</>
);
}
このとき/users/..%2fadminUser
にアクセスすると、パラメータuserId
の値は../adminUser
になり、①でパストラバーサルが発生して/api/users/:userId
ではなく/api/adminUser
に対してAPI呼び出しが行われます。
これによって、CSRFに似た次のような攻撃が考えられます:
- 被害者に特定の罠URLをアクセスさせる。
- 被害者の権限(例: セッションクッキーや認証ヘッダ)の元、パストラバーサルで任意のURLパスに対してAPI呼び出しを強制。APIのメソッドがPOSTやDELETEだった場合はデータへの作用が可能なこともあります。
- API呼び出しの後続の処理によってはXSS等のさらなる被害に拡大。
クライアントサイドにおけるパストラバーサルについては「Security.Tokyo #3」で発表された以下のスライドでわかりやすく言及されているため、参考として紹介します:
依存ライブラリが暗黙的に行うURL正規化
アプリケーション開発者がHTTPリクエストを送信するプログラムを直接実装していない場合でも、依存ライブラリが内部で行うHTTP通信で意図せずURL正規化が行われてしまうことがあります。これが原因でパストラバーサル脆弱性が発生してしまう可能性にも注意が必要です。
以下は、クエリパラメータkey
で指定されたS3オブジェクトを取得してその内容をクライアントに返却するPHPプログラムです。S3と連携するにあたってAWS SDK for PHPの公式ライブラリを利用しています。
<?php
require "vendor/autoload.php";
use Aws\S3\S3Client;
$s3 = new S3Client([
"credentials" => [
"key" => $_ENV["AWS_ACCESS_KEY_ID"],
"secret" => $_ENV["AWS_SECRET_ACCESS_KEY"],
],
"region" => $_ENV["AWS_REGION"],
]);
$PREFIX = "images/";
$result = $s3->getObject([
"Bucket" => $_ENV["AWS_S3_BUCKET"],
"Key" => $PREFIX . $_GET["key"],
]);
echo $result["Body"];
文字列結合によってキーにはimages/
のプレフィックスが必ず付与されるようになっています。ここで、?key=../secret.txt
のようなクエリパラメータを指定した場合の挙動について考えてみましょう。このときパストラバーサルによって、キーがsecret.txt
のS3オブジェクトにアクセスするようになるでしょうか?
S3の仕様をある程度知っている人は、「S3オブジェクトのキーやプレフィックスでは../
のような文字列は特別な意味を持たないため、パストラバーサルは発生しない」と考えるかもしれません。仕様に従えば、キーがimages/../secret.txt
のオブジェクトに対してアクセスする挙動が妥当と言えるでしょう。実際、S3バケット内はフラットな構造になっており、ファイルシステムにあるような階層の概念は存在しません。
[4] 厳密には、AWSのコンソール上でS3オブジェクトのグループ化が可能な「フォルダ」の概念があります(参考リンク)。
ところが、上記プログラムを実際に動かしたところ、キーがsecret.txt
のS3オブジェクトにアクセスする挙動が確認されました。というのもAWS SDK for PHPはS3オブジェクトにアクセス時に内部でGuzzleのHTTPクライアントが利用されており、暗黙的にURLの正規化処理が行われてしまうようになっていました。
「S3ではパストラバーサルは発生しない」と思い込んでいる開発者にとっては罠のような挙動であり、上記は依存ライブラリが暗黙的に行うURL正規化が原因のパストラバーサルの例となります。
なお、見方によればS3の仕様に反する挙動であり脆弱性と言えなくもないため、念の為AWSのセキュリティチームに報告し、すでに最新版のバージョンにおいては上記挙動に関して修正がされています。
該当のライブラリを利用されている方は、修正バージョン以上にアップグレードすることを推奨します。
おわりに
URL正規化によるパストラバーサルに焦点を当てて攻撃の発生原理や発生例について紹介しました。「パストラバーサルはファイルシステム内の任意ファイルにアクセスできる脆弱性である」と認識していると、思いがけないところでパストラバーサルの脆弱性を作り込んでしまうかもしれません。しかし、いずれのパストラバーサルにおいても「ユーザの入力値を信用してはいけない」という基本的なセキュリティの考え方は通用するため、そのことを念頭に置きながらアプリケーションの開発を行うことがまずは大切だと思います。
GMOサイバーセキュリティ byイエラエではバグバウンティやセキュリティコンテストなどで活躍するセキュリティエンジニアが実施する「Webペネトレーションテスト <シナリオ型> / <調査型>」を提供しています。標準的なWebアプリケーション診断では検出ができないような脆弱性も検出し、リスクを評価します。また、ソースコードを共有いただくことによってより精度の高い調査を実施することが可能です。是非お気軽にご相談ください。