0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LLMのSQL実行、SQLiteで機密列の参照を止める

0
Posted at

SELECTだけ許しても、メールアドレスは読める。LLMにSQLを作らせるなら、「更新しない」に加えて、読ませる列をDB接続側で絞りたい。

手元では、名前だけ返すSQLのWHEREにメールアドレスを入れた。自分はここも止めたい。

SQLiteで列アクセスを拒否する

Pythonのset_authorizer()に判定関数を渡す。main.users.nameの読み取りとcountを許可し、ほかは拒否する。LLMには接続せず、SQL実行部分を検証する。

Python 3.14.6/SQLite 3.53.4で実行済み。column_guard.pyに保存し、python3 column_guard.pyで動く。追加パッケージは不要。DBはメモリ内に作る。

import sqlite3

db = sqlite3.connect(":memory:", cached_statements=0)
db.executescript("""
CREATE TABLE users(name, email);
INSERT INTO users VALUES('Aki', 'aki@example.test');
""")
print("制限前:", db.execute("SELECT email FROM users").fetchall())

def authorize(action, a, b, schema, source):
    if action == sqlite3.SQLITE_SELECT:
        return sqlite3.SQLITE_OK
    if action == sqlite3.SQLITE_READ:
        if (schema, a, b) == ("main", "users", "name"):
            return sqlite3.SQLITE_OK
    if action == sqlite3.SQLITE_FUNCTION and b == "count":
        return sqlite3.SQLITE_OK
    return sqlite3.SQLITE_DENY

db.set_authorizer(authorize)
queries = [
    "SELECT name FROM users",
    "SELECT email FROM users",
    "SELECT name FROM users WHERE email='aki@example.test'",
    "SELECT count(*) FROM users",
    "SELECT count(name) FROM users",
]
for i, sql in enumerate(queries, 1):
    try:
        print(i, db.execute(sql).fetchall())
    except sqlite3.DatabaseError as e:
        print(i, type(e).__name__ + ":", str(e))
db.close()
制限前: [('aki@example.test',)]
1 [('Aki',)]
2 DatabaseError: access to users.email is prohibited
3 DatabaseError: access to users.email is prohibited
4 DatabaseError: not authorized
5 [(1,)]

3番も止まった。

SQLiteはSQLのコンパイル時に認可関数を呼ぶ。SQLITE_READではDB名・テーブル名・列名を見られるので、WHERE内の参照も判定できる。SQLITE_DENYで文全体を拒否する仕様は公式資料で確認した。

countの許可だけでは足りなかった

4番は少し意外だった。関数のcountは許している。それでも止まる。

公式資料によると、count(*)のように列値を取り出さない参照では、SQLITE_READに空の列名が渡る。手元の引数は(None, 'users', '')だった。許可条件には合わない。検証ではキャッシュを無効にし、毎回コンパイルさせている。

5番は許可したnameを読むので通る。ただし、count(name)はNULLを数えない。全行数が欲しいなら、公開してよいNOT NULL列を用意するなど、集計の意味まで決める必要がある。名前が必須かどうか、ここでスキーマ設計に戻ってくる。

SQL実行ツールに持ち帰るなら

許可列の取得と機密列を使う条件式を、セットで回帰テストに残したい。この例は列単位の制限なので、行ごとの閲覧権限や実行時間には別の対策が要る。認可関数は接続ごとに登録し、未信頼SQLを渡す前から固定して使う。

SQL生成が上手くなるほど、正しく動くSQLの中に読ませたくない列が混じる場面も増えそうだ。返る表だけ見て安心せず、条件式で触る列までテストで押さえておく。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?