一覧SC-02-2 · 脅威モデリング

偽装・改変・権限違反への対策

検査する値と使う値、署名対象、認可条件をそれぞれ特定し、対策が防ぐ経路と残る条件を説明します。

説明

攻撃経路を検討するときは、名前だけを分類するより、入力がどの処理へ届き、どの検査が抜けると何が起きるかを具体化します。署名の検証と業務上の認可も、守る条件が異なります。

注文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鍵

1
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'}
False
2
from 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
3
changed = body.replace(b"100", b"999") print(verify(changed, tag))
False
4
spaced = b'{"order": 7, "amount": 100}' print(verify(spaced, tag))
False
5
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)
6
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)
7
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)]

演習完了