偽装・改変・権限違反への対策
検査する値と使う値、署名対象、認可条件をそれぞれ特定し、対策が防ぐ経路と残る条件を説明します。
説明
攻撃経路を検討するときは、名前だけを分類するより、入力がどの処理へ届き、どの検査が抜けると何が起きるかを具体化します。署名の検証と業務上の認可も、守る条件が異なります。
注文APIの固定構成で、申告ヘッダー、決済通知、テナント別の注文を調べます。公開試料鍵で実際にHMACを計算し、通知改変を検出します。その後、SQLiteの更新条件にテナントを含める前後で保存結果を比較します。
検査する値と使う値、署名対象、認可条件をそれぞれ特定し、対策が防ぐ経路と残る条件を説明します。
1入力元の違う主体情報
peerは接続元、claimedは要求が名乗るユーザー、authenticatedは認証処理で確認した利用者です。申告値だけで権限を決めてはいけません。
request =入力元の異なる主体情報を用意する {"peer": "203.0.113.20", "claimed": "admin", "authenticated": "alice"}
print(request)三つの値を並べる
print(request["claimed"] == request["authenticated"]申告と認証結果を比較する)adminとaliceは一致しません。HTTPヘッダーで名乗れることと、その主体として認証されたことは異なります。
2通知のバイト列を署名する
このHMAC鍵は公開の検算用です。HMACは共有鍵を持つ相手とのメッセージ認証に使い、鍵を持たない第三者には同じ検証能力を渡しません。
from sc02 import connect, sign, verifyDBと公開試料HMACを読み込む
body = b'{"order":7,"amount":100}'
tag = sign(body)通知のバイト列へ認証タグを付ける
print(body.hex())署名対象のバイト列を表示する
print(tag)認証タグを読む
print(verify(body, tag))元の通知を検証する元のバイト列ではTrueです。署名の対象は表示されたJSONの意味だけではなく、そのバイト列です。
3金額だけを変える
元のタグを保持したまま、通知の金額100を999へ変えます。
changed = body.replace(b"100", b"999")本文の金額を変更する
print(verify(changed, tag)元のタグで変更後の本文を検証する)Falseです。受信した本文とタグが対応しないため、元の送信者が認証した内容として受け入れられません。
4意味が同じでもバイト列が違う
空白を追加したJSONは同じ値を表しますが、この署名方式ではバイト列が変わります。
spaced =同じ意味で空白の異なるJSONを用意する b'{"order": 7, "amount": 100}'
print(verify(spaced, tag)バイト列の違いを検証する)Falseです。検証前に本文を再整形すると、送信側が署名したバイト列との対応を失う場合があります。
5IDだけで更新する問題
redの利用者が注文8を指定した状況を考えます。注文8はblueですが、IDだけのUPDATEはその行を変更できます。
db = connect()独立したテナント別注文を用意する
print(db.execute("SELECT * FROM orders ORDER BY id元の所有関係を読む").fetchall())
db.execute("UPDATE orders SET amount=999 WHERE id=8IDだけで更新する")
print(db.execute("SELECT * FROM orders WHERE id=8変更された他テナントの行を読む").fetchone())blueの注文8が999へ変わります。IDの存在確認や署名の成功だけでは、redがその注文を更新してよいかを決められません。
6更新条件に所属を含める
元の200へ戻した後、サーバーで確認した所属redを更新条件へ加えます。
db.execute("UPDATE orders SET amount=200比較のため元の値へ戻す WHERE id=8")
changed_rows = db.execute("UPDATE orders SET amount=? WHERE id=? AND tenant=?IDと認証済み所属で更新対象を限定する", (999, 8, "red")).rowcount
print(changed_rows)更新件数を読む
print(db.execute("SELECT * FROM orders WHERE id=8他テナントの値が保持されたことを読む").fetchone())更新0件で、blueの200が保持されます。値を安全に束縛することと、tenant条件を付けることは別の防御で、両方が必要です。
7同じ所属の注文は更新できる
redに属する注文7へ、同じ条件付きUPDATEを使います。
print(db.execute("UPDATE orders SET amount=? WHERE id=? AND tenant=?redの注文だけを更新する", (150, 7, "red")).rowcount)
print(db.execute("SELECT * FROM orders ORDER BY id正当な更新と他テナント保持を照合する").fetchall())更新1件で、redは150、blueは200です。すべて拒否するのではなく、正当な操作を保ちながら対象を限定できました。
まとめ
認証済みの主体は、入力された申告値とは別に確認します。
HMACは対象バイト列の変更を検出しますが、業務上の権限を決めるものではありません。
更新条件に認証済み所属を含め、許可対象と保存結果を照合します。
操作履歴と解説を振り返れます。
Python 3.15 / SQLite / 固定構成と公開試料HMAC鍵
request = {"peer": "203.0.113.20", "claimed": "admin", "authenticated": "alice"}
print(request)
print(request["claimed"] == request["authenticated"]){'peer': '203.0.113.20', 'claimed': 'admin', 'authenticated': 'alice'}
Falsefrom sc02 import connect, sign, verify
body = b'{"order":7,"amount":100}'
tag = sign(body)
print(body.hex())
print(tag)
print(verify(body, tag))7b226f72646572223a372c22616d6f756e74223a3130307d bf90a1a13bb15ac284e88d20c984d27cab9c7bba7f416a91a6e28ae8ad34c5f9 True
changed = body.replace(b"100", b"999")
print(verify(changed, tag))False
spaced = b'{"order": 7, "amount": 100}'
print(verify(spaced, tag))False
db = connect()
print(db.execute("SELECT * FROM orders ORDER BY id").fetchall())
db.execute("UPDATE orders SET amount=999 WHERE id=8")
print(db.execute("SELECT * FROM orders WHERE id=8").fetchone())[(7, 'red', 100), (8, 'blue', 200)] (8, 'blue', 999)
db.execute("UPDATE orders SET amount=200 WHERE id=8")
changed_rows = db.execute("UPDATE orders SET amount=? WHERE id=? AND tenant=?", (999, 8, "red")).rowcount
print(changed_rows)
print(db.execute("SELECT * FROM orders WHERE id=8").fetchone())0 (8, 'blue', 200)
print(db.execute("UPDATE orders SET amount=? WHERE id=? AND tenant=?", (150, 7, "red")).rowcount)
print(db.execute("SELECT * FROM orders ORDER BY id").fetchall())1 [(7, 'red', 150), (8, 'blue', 200)]
演習完了