一覧SD-11-3 · APIとブラウザ境界の防御

HTTPリクエストの解釈の不一致

中継前後で同じ対象と本文を使うための検査条件を説明します。

説明

中継装置とアプリで同じHTTP要求を異なる形に解釈すると、宛先や要求の区切りがずれる場合があります。重複したHostや、本文長を表す複数の情報を曖昧なまま転送しないようにします。

固定のHTTP/1.1バイト列を標準ヘッダーパーサーで読み、最初・最後の値を取る処理の違いを比較します。次にこのAPIの限定的な受入契約を適用し、Hostと本文の長さが一つに定まる要求だけを通します。

中継前後で同じ対象と本文を使うための検査条件を説明します。

1要求行・ヘッダー・本文を分解する

本文item=penは8バイトです。

from sd11 import * wire = request(["Host: api.example.test","Content-Length: 8"]) method,target,version,headers,body = parse(wire)生の要求を構成要素へ分解する print(method,target,version) print(list(headers.items()),body,len(body)) print(strict(wire)限定した受入契約へ照合する)

POST /orders、単一Host、長さ8の本文へ分かれます。受入後も同じHostと本文を渡します。

2重複Hostで値の選び方が変わる様子を見る

同じ名前のヘッダーが二つある試料を作ります。

duplicate = request(["Host: api.example.test","Host: other.example.test","Content-Length: 8"]) values = parse(duplicate)[3].get_all("Host")重複値を捨てずに取得する print(values[0],values[-1]値の選択方法による違いを示す) print(strict(duplicate))

最初と最後で宛先が違い、受入器はhost_countで拒否します。中継とアプリが別の値を選ぶ余地を残しません。

3Hostと要求ターゲットの受入形式を固定する

このAPIはorigin-formのパスだけを受け付けます。別Hostと絶対URL形式を比較します。

print(strict(request(["Host: other.example.test"宛先Hostを変える,"Content-Length: 8"]))) print(strict(request(["Host: api.example.test","Content-Length: 8"],target="http://other.example.test/orders"絶対URL形式のターゲットを渡す)))

host_rejectedとtarget_rejectedです。汎用forward proxyの全形式を扱う例ではなく、固定APIの受入形式を限定しています。

4重複Content-Lengthを拒否する

8と3を同じ要求へ入れます。

conflict = request(["Host: api.example.test","Content-Length: 8","Content-Length: 3"]) lengths = parse(conflict)[3].get_all("Content-Length")本文長の候補をすべて取得する print(lengths) print(body[:int(lengths[0])],body[:int(lengths[-1])]異なる境界で区切った本文を比較する) print(strict(conflict))

最初の値を採用するとitem=pen、最後の値を採用するとiteになります。ここではlength_rejectedとして拒否します。同じ値の重複も拒否する仕様です。

5Transfer-Encodingとの混在を拒否する

このAPIは固定長だけを扱い、Transfer-Encodingを受け付けません。

mixed = request(["Host: api.example.test","Content-Length: 8","Transfer-Encoding: chunked"本文の別の区切り方を同時に指定する]) print(strict(mixed))

transfer_encoding_rejectedです。HTTP全般でchunkedを禁止する規則ではなく、この受入経路の契約です。両方を受け付ける中継ではRFCに沿った処理と下流への正規化を揃えます。

6不足・余剰・上限超過の本文を比較する

固定長と実バイト列の不一致を調べます。

for length in (7,9,17)実本文と異なる申告長を与える: print(length,strict(request(["Host: api.example.test",f"Content-Length: {length}"])))

7と9はbody_length_mismatch、17はbody_limitです。この試料は受信済みバイト列の照合です。ストリーム受信器では長さに応じて読み、余剰を次要求とするか切断するかも統一します。

7受入後の値から下流用要求を再構成する

正常要求の受入結果を使い、重複しない下流向け要求を組み立てます。

accepted = strict(wire) method,target,host,text = accepted forwarded = request受入済みの値から単一ヘッダーを再構成する([f"Host: {host}",f"Content-Length: {len(text.encode())}"],text.encode(),target) print(strict(forwarded)) print(wire==forwarded固定試料の一致を確認する)

再解析結果とバイト列が一致します。実中継の全挙動は実装ごとに確認しますが、この資料では検査した値と下流へ使った値を一致させています。

まとめ

重複値を捨てる前に曖昧さを検出します。

Host・ターゲット・本文長の解釈を中継前後で揃えます。

固定バイト列の検査と実ストリームの区切りを混同しません。

操作履歴と解説を振り返れます。

Python 3.15 / 固定の公開試料

1
from sd11 import * wire = request(["Host: api.example.test","Content-Length: 8"]) method,target,version,headers,body = parse(wire) print(method,target,version) print(list(headers.items()),body,len(body)) print(strict(wire))
POST /orders HTTP/1.1
[('Host', 'api.example.test'), ('Content-Length', '8')] b'item=pen' 8
('POST', '/orders', 'api.example.test', 'item=pen')
2
duplicate = request(["Host: api.example.test","Host: other.example.test","Content-Length: 8"]) values = parse(duplicate)[3].get_all("Host") print(values[0],values[-1]) print(strict(duplicate))
api.example.test other.example.test
host_count
3
print(strict(request(["Host: other.example.test","Content-Length: 8"]))) print(strict(request(["Host: api.example.test","Content-Length: 8"],target="http://other.example.test/orders")))
host_rejected
target_rejected
4
conflict = request(["Host: api.example.test","Content-Length: 8","Content-Length: 3"]) lengths = parse(conflict)[3].get_all("Content-Length") print(lengths) print(body[:int(lengths[0])],body[:int(lengths[-1])]) print(strict(conflict))
['8', '3']
b'item=pen' b'ite'
length_rejected
5
mixed = request(["Host: api.example.test","Content-Length: 8","Transfer-Encoding: chunked"]) print(strict(mixed))
transfer_encoding_rejected
6
for length in (7,9,17): print(length,strict(request(["Host: api.example.test",f"Content-Length: {length}"])))
7 body_length_mismatch
9 body_length_mismatch
17 body_limit
7
accepted = strict(wire) method,target,host,text = accepted forwarded = request([f"Host: {host}",f"Content-Length: {len(text.encode())}"],text.encode(),target) print(strict(forwarded)) print(wire==forwarded)
('POST', '/orders', 'api.example.test', 'item=pen')
True

演習完了