一覧SD-01-2 · 入力検証と解釈の境界

重複パラメーターと入力の正規化

正規化する順序と回数を決め、検証済みの値をそのまま利用する設計を学びます。

説明

検証側と利用側が同じ要求を違う規則で解釈すると、検査を通った値と実際に使う値が食い違います。重複キーを先頭優先・末尾優先で扱う差や、URLの二重デコードが代表的な原因です。

一つの入力を標準のパーサーで解析し、解釈の違いを比較します。重複の拒否、一度だけのデコード、Unicodeの正規化を行い、保存する値まで同じ表現を使う流れを確認します。

正規化する順序と回数を決め、検証済みの値をそのまま利用する設計を学びます。

1同名パラメーターを一覧として読む

数量を二つ含むクエリーです。解析直後は重複を保持する形式で確認します。

from urllib.parse import parse_qsl,unquote from sd01 import parse_once,unique import json,unicodedata,sqlite3 query = "quantity=1&quantity=9" pairs = parse_qsl(query,keep_blank_values=True)重複と空値を保持して解析する print(pairs)

quantityが1と9の二つあります。ここでdictへ変換すると、どちらか一つしか残りません。

2先頭優先と末尾優先を比較する

検査側が先頭の1、利用側がdict変換後の9を使う場合です。

checked = pairs[0][1]先頭の値を検査側の値にする used = dict(pairs)["quantity"]末尾が残る変換で利用側の値を取得する print(checked,used,int(checked)<=5,int(used)<=5)

検査した1は範囲内ですが、使う9は範囲外です。各段階の規則が違うことが原因です。

3単一値の項目では重複を拒否する

parse_onceは一覧を受け、同名キーがあれば辞書へ変換する前に拒否します。

print(parse_once(query)重複したキーを拒否する) print(parse_once("quantity=1")) print(parse_once("quantity=")空値を保持して解析する)

重複はduplicate:quantity、単一の値は辞書になります。空文字を保持することで、欠落と空の値も後で別に検証できます。

4二重に符号化されたパスを比較する

%25は%の符号化です。二回のデコードで初めて..が現れる試料を使います。

raw = "%252e%252e%252fnotes.txt" once = unquote(raw)一回目のデコードを行う twice = unquote(once)二回目との違いを確認する print(raw,once,twice) print(".." in once,".." in twice)

一回目は%2e%2e%2f、二回目は../です。検査後にもう一度デコードする構成では、利用時に別のパスへ変わってしまいます。

5JSONの重複キーも保持して調べる

JSONの同名キーを通常のloadsで読むと、ここでは末尾が残ります。object_pairs_hookで解析直後の対を受け取れます。

raw_json = '{"quantity":1,"quantity":9}' print(json.loads(raw_json)通常の辞書変換を確認する) print(json.loads(raw_json,object_pairs_hook=list重複した対を保持する)) try: json.loads(raw_json,object_pairs_hook=unique重複を拒否して変換する) except ValueError as error: print(str(error))

通常は9だけが残り、重複拒否ではduplicate:quantityです。入力形式が変わっても解釈の一致を確かめる必要があります。

6Unicodeの見た目とコードポイントを比較する

éの合成済み表現と、e+結合アクセントの表現を使います。

a = "é" b = "é" print(a==b元のコードポイント列を比較する,len(a),len(b),a.encode().hex(),b.encode().hex()) print(unicodedata.normalize("NFC",a)合成済みの正規形へそろえる==unicodedata.normalize("NFC",b))

元は1文字分と2コードポイントで異なりますが、NFC後は一致します。大小文字やすべての見た目の類似までNFCが統一するわけではありません。

7正規化した値を一意に保存する

このシステムはNFCで同じ名前を同一と扱う仕様にします。検査後も同じ正規化済み値を保存します。

db = sqlite3.connect(":memory:") db.execute("CREATE TABLE names(value TEXT UNIQUE保存値の一意性を設定する)") print(db.execute("INSERT OR IGNORE INTO names VALUES(?)",(unicodedata.normalize("NFC",a)正規化済みの値を保存する,)).rowcount) print(db.execute("INSERT OR IGNORE INTO names VALUES(?)",(unicodedata.normalize("NFC",b),)).rowcount) print(db.execute("SELECT value FROM names").fetchall())

初回は1、二つ目は0で一行です。正規化の規則を仕様として定め、検証・保存・検索で同じ表現を使います。

まとめ

解析段階で重複を把握し、単一値項目の扱いを決めます。

検証後の再デコードで値を変えない構成にします。

正規化した値を、検証から保存まで一貫して使います。

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

Python 3.15 / HTTP・SQLite・標準パーサー / 固定の要求

1
from urllib.parse import parse_qsl,unquote from sd01 import parse_once,unique import json,unicodedata,sqlite3 query = "quantity=1&quantity=9" pairs = parse_qsl(query,keep_blank_values=True) print(pairs)
[('quantity', '1'), ('quantity', '9')]
2
checked = pairs[0][1] used = dict(pairs)["quantity"] print(checked,used,int(checked)<=5,int(used)<=5)
1 9 True False
3
print(parse_once(query)) print(parse_once("quantity=1")) print(parse_once("quantity="))
duplicate:quantity
{'quantity': '1'}
{'quantity': ''}
4
raw = "%252e%252e%252fnotes.txt" once = unquote(raw) twice = unquote(once) print(raw,once,twice) print(".." in once,".." in twice)
%252e%252e%252fnotes.txt %2e%2e%2fnotes.txt ../notes.txt
False True
5
raw_json = '{"quantity":1,"quantity":9}' print(json.loads(raw_json)) print(json.loads(raw_json,object_pairs_hook=list)) try: json.loads(raw_json,object_pairs_hook=unique) except ValueError as error: print(str(error))
{'quantity': 9}
[('quantity', 1), ('quantity', 9)]
duplicate:quantity
6
a = "é" b = "é" print(a==b,len(a),len(b),a.encode().hex(),b.encode().hex()) print(unicodedata.normalize("NFC",a)==unicodedata.normalize("NFC",b))
False 1 2 c3a9 65cc81
True
7
db = sqlite3.connect(":memory:") db.execute("CREATE TABLE names(value TEXT UNIQUE)") print(db.execute("INSERT OR IGNORE INTO names VALUES(?)",(unicodedata.normalize("NFC",a),)).rowcount) print(db.execute("INSERT OR IGNORE INTO names VALUES(?)",(unicodedata.normalize("NFC",b),)).rowcount) print(db.execute("SELECT value FROM names").fetchall())
1
0
[('é',)]

演習完了