はじめに
Azure Container Apps は、サーバーレスなコンテナ実行環境として多くのユースケースに対応する一方、セキュリティ向上のために HTTP リクエストの URL に対して自動的な正規化・デコード処理を実施しています。
目立たないですが、Docs にも以下のように記載されています。
アプリが受信する URL が、要求で指定された URL と異なるのはなぜですか?
Azure Container Apps により URL はデコードされ、URL 混乱攻撃からアプリが保護されます。 http://mysite.com/archive/http%3A%2F%2Fmysite.com%2Farchive%2F123 のようにエンコードされた部分を含む要求 URL は、http://mysite.com/archive/http%3A/mysite.com/archive/123 としてアプリに送信されます。
これにより、アプリケーションが受信する URL が、クライアントから送信されたものと異なる場合があります。
本記事では、この URL フィルタリングの仕組みとその背景、そして開発者が考慮すべきポイントについて説明します。
URL フィルタリングの仕組み
Azure Container Apps の HTTP イングレスでは、Envoy プロキシがフロントエンドとして機能します。
Envoy は、受信したリクエストの URL を自動的にデコードし、正規化を行います。
例えば、URL 内のエンコードされた特殊文字や余計なパーセントエンコーディングを解除することで、内部的に統一された形式でアプリケーションへ渡されるようになります。
この処理により、URL 混乱攻撃 (URL Confusion Attack) を未然に防ぐ効果が期待されます。つまり、セキュリティを向上させるための自動フィルタリング機能として働いているのです。
URL混乱攻撃 (URL Confusion Attack) の例
例えば、あるセキュリティポリシーが /admin
へのアクセスを制限しているとします。
しかし、クライアントが /%61dmin
や //admin
等のエンコードされたURLを送信した場合、プロキシやアプリケーションがこれを正規化して /admin
として処理するか、またはそのままの状態で扱うかによって、判断が分かれます。
このように、各システムコンポーネントがURLのエンコードや正規化処理に差異があると、攻撃者はそのギャップを利用してアクセス制御を回避することが可能となります。
サンプルアプリ
簡単なサンプルアプリで動作確認をしてみます。
$ ls -l
-rw-r--r-- 1 root root 444 Feb 12 2025 Dockerfile
-rw-r--r-- 1 root root 916 Feb 12 2025 request-logger.py
-rw-r--r-- 1 root root 5 Feb 12 2025 requirements.txt
FROM python:3.11-slim-bookworm
RUN python3 -m pip install --no-cache-dir --upgrade pip && \
python3 -m pip install --no-cache-dir \
Flask
WORKDIR /app
ADD . /app
EXPOSE 8080
CMD ["python3", "request-logger.py"]
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
@app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
def log_request(path):
# Collect request details
method = request.method
url = request.url
headers = dict(request.headers)
body = request.get_data(as_text=True)
# Prepare the response
response = {
'method': method,
'url': url,
'headers': headers,
'body': body
}
# Send the response back to the client
return jsonify(response)
if __name__ == '__main__':
app.run(port=8080, host="0.0.0.0", debug=False)
Flask
このサンプルアプリを Container Apps にデプロイし、動作確認をします。
試しに、https://<Container Apps のイングレス URL>/something/redirect/http%3A%2F%2Fschema.org%2Fresources%2F123
というエンドポイントに対して GET リクエストをブラウザから送ってみます。
すると、以下のレスポンスが返ってきました。
肝となるのは、出力された url
です。受信したリクエストの URL が自動的にデコードされ、正規化されていることがわかります。
- リクエストした URL:
https://<Container Apps のイングレス URL>/something/redirect/http%3A%2F%2Fschema.org%2Fresources%2F123
- 出力された URL:
https://<Container Apps のイングレス URL>/something/redirect/http:/schema.org/resources/123
開発者が留意すべき点
この自動デコード処理は、セキュリティ向上には寄与しますが、一方で以下の3点に注意する必要があると考えます。
- ログや解析時の相違
クライアントから送信された URL と、アプリケーションが実際に受け取る URL に相違が生じるため、ログ出力やデバッグ時に混乱する可能性があります。
例えば、Node.js (Express) では以下のように実装することで、クライアントから送信された URL と、アプリケーションが実際に受け取る URL を同一にすることができます。
const express = require('express');
const app = express();
app.use((req, res, next) => {
// req.url は Envoy によりデコード・正規化された値が入る場合がある
console.log('Normalized URL:', req.url);
// もし必要なら、再エンコードして比較用に利用する
const reEncodedUrl = encodeURI(req.url);
console.log('Re-encoded URL:', reEncodedUrl);
next();
});
app.get('/api/secret', (req, res) => {
res.send('Secret data');
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
- アプリケーションのパスマッチング
URL パスをキーとして動作するルーティングやフィルタリング処理を実装している場合、エンコード前後の違いに注意し、必要に応じて追加の処理(再エンコードなど)を検討します。
Container Apps では、リクエストヘッダの Referer
から元のリクエスト URL を取得できます。
app.use((req, res, next) => {
const originalUrl = req.header('Referer');
if (originalUrl) {
console.log(`Original URL from header: ${originalUrl}`);
} else {
console.warn('Referer header is missing.');
}
next();
});
上記のサンプルアプリでも、リクエストヘッダ Referer
に元のリクエスト URL が入っていることがわかります。
- セキュリティポリシーとの整合性
自動正規化により、意図しないパスへのアクセスがブロックされる場合もあるため、セキュリティポリシーやアクセス制御ルールとの整合性を確認することが重要です。
以下の例では、/admin
へのアクセス時に Authorization
ヘッダーの値が特定のトークン(例:Bearer expected-token
)であるかを確認し、条件を満たさない場合は 403 Forbidden
を返します。
app.use((req, res, next) => {
// Envoy により正規化された URL で /admin へのアクセスをチェック
if (req.url === '/admin') {
const authHeader = req.header('Authorization');
if (!authHeader || authHeader !== 'Bearer expected-token') {
console.warn('Access denied to /admin due to missing or invalid Authorization header.');
return res.status(403).send('Forbidden');
}
}
next();
});
app.get('/admin', (req, res) => {
res.send('Welcome to the admin area');
});
おわりに
本記事では、Azure Container Apps に搭載されている URL フィルタリングの仕組みとその背景、そして開発者が考慮すべきポイントについて説明しました。
URL 混乱攻撃に対する対策を念頭に入れ、システム全体のセキュリティを向上させつつ、開発者として Container Apps を利用する際にお役立ちできれば幸いです。
※なお、本記事の実装はあくまでサンプルです。実開発においては、要件に応じて最適な設計・実装を行っていただきますようお願いいたします。