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

APIの更新項目とレスポンスの制限

入力スキーマ・更新権限・出力項目・取得量をそれぞれ確認します。

説明

APIがJSONを受け取るとき、DBの列へ入力項目を一括反映すると、画面にない権限情報まで変更できる場合があります。入力の項目と、更新してよい項目をアプリ側で決めます。

aliceの表示名を変更する小さなAPIを使い、roleを追加した要求を比較します。その後、出力の項目と一覧件数を制限し、拒否後の保存状態を照合します。

入力スキーマ・更新権限・出力項目・取得量をそれぞれ確認します。

1表示名と権限を持つ元表を用意する

本人に許すのは表示名nameの更新だけです。roleとemailはこの操作の対象外です。

from sd11 import * lab = API() print(lab.db.execute("SELECT * FROM users元の名前・権限・メールを照合する ORDER BY id").fetchall())

aliceはviewer、bobはeditorです。以後は認証済み主体aliceのID1をサーバー側で指定する条件にします。

2一括反映が余分な項目も更新する例を試す

表示名のほかにroleを含むJSONを、入力項目を一括反映する処理へ渡します。

payload = json.dumps({"name":"Alice","role":"admin"}) lab.unsafe入力項目をそのまま列へ反映する(1,payload) print(lab.db.execute("SELECT name,role FROM users WHERE id=1").fetchone())

roleがadminへ変わりました。SQLをパラメーター化しても、更新してよい項目を制限しない問題は残ります。

3更新項目の許可リストへ変更する

元のviewerへ戻し、nameだけを受け付ける処理へ同じJSONを渡します。

lab.db.execute("UPDATE users SET name='alice',role='viewer' WHERE id=1") lab.db.commit() print(lab.update許可項目と型を検査してから更新する(1,payload)) print(lab.db.execute("SELECT name,role FROM users WHERE id=1").fetchone())

400でalice/viewerを保ちます。余分な項目は黙って無視せず、この契約では要求全体を拒否します。

4通常の変更と型違いを比較する

正しいnameの後、配列をnameへ渡します。

print(lab.update(1,json.dumps({"name":"Alice"})許可された文字列の変更を送る)) print(lab.update(1,json.dumps({"name":["admin"]})値の型を変えた要求を送る)) print(lab.db.execute("SELECT id,name,role FROM users ORDER BY id").fetchall())

正常な変更は200、型違いは400です。aliceの表示名だけが変わり、roleとbobを維持します。

5返却列も明示して必要な情報だけ出す

公開プロフィールの返却対象はidとnameです。

print(lab.public(1)公開用に選択した列を取得する) print(lab.db.execute("SELECT email非公開列が元表に残ることを照合する FROM users WHERE id=1").fetchone())

公開結果にemailは含まれませんが、元表には残っています。保存した情報をすべて直列化してから隠すより、取得・出力の段階で必要な列を選びます。

6一覧件数を制限し次の範囲を取得する

itemsは5件。最大2件でid順に取得し、最後のIDを次のafterへ渡します。

print(lab.list(2)上限内で最初の範囲を取得する) print(lab.list(2,after=2前回の最後のIDより後を取得する)) print(lab.list(2,after=4))

2件、2件、1件と返ります。返却件数だけでなく、DBのLIMITへ制約を渡しています。ID順のこの例では重複せず範囲を続けられます。

7無制限取得につながる値を拒否する

負数、上限超過、文字列を比較します。

for limit in件数の値と型を変えて拒否を確認する (-1,3,"2",True): print(repr(limit),lab.list(limit)) print(lab.db.execute("SELECT COUNT(*) FROM items").fetchone())

すべて400で返却行は空、元の5件は維持されています。複雑な集計や関連展開を提供するAPIでは件数に加え、展開深さや計算量の制限も必要です。

まとめ

SQLの値束縛と、更新できる項目の認可は別に設けます。

返却する列と件数を明示します。

拒否後も権限・他者の行・元データが保たれることを調べます。

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

Python 3.15 / 固定の公開試料

1
from sd11 import * lab = API() print(lab.db.execute("SELECT * FROM users ORDER BY id").fetchall())
[(1, 'alice', 'viewer', 'alice@example.test'), (2, 'bob', 'editor', 'bob@example.test')]
2
payload = json.dumps({"name":"Alice","role":"admin"}) lab.unsafe(1,payload) print(lab.db.execute("SELECT name,role FROM users WHERE id=1").fetchone())
('Alice', 'admin')
3
lab.db.execute("UPDATE users SET name='alice',role='viewer' WHERE id=1") lab.db.commit() print(lab.update(1,payload)) print(lab.db.execute("SELECT name,role FROM users WHERE id=1").fetchone())
400
('alice', 'viewer')
4
print(lab.update(1,json.dumps({"name":"Alice"}))) print(lab.update(1,json.dumps({"name":["admin"]}))) print(lab.db.execute("SELECT id,name,role FROM users ORDER BY id").fetchall())
200
400
[(1, 'Alice', 'viewer'), (2, 'bob', 'editor')]
5
print(lab.public(1)) print(lab.db.execute("SELECT email FROM users WHERE id=1").fetchone())
[(1, 'Alice')]
('alice@example.test',)
6
print(lab.list(2)) print(lab.list(2,after=2)) print(lab.list(2,after=4))
(200, [(1, 'pen'), (2, 'book')])
(200, [(3, 'paper'), (4, 'case')])
(200, [(5, 'bag')])
7
for limit in (-1,3,"2",True): print(repr(limit),lab.list(limit)) print(lab.db.execute("SELECT COUNT(*) FROM items").fetchone())
-1 (400, [])
3 (400, [])
'2' (400, [])
True (400, [])
(5,)

演習完了