静的解析によるSQLインジェクションの検出
静的な検出候補と、実行で確認した挙動、未解析部分を分けて説明します。
説明
SASTはコードを実行せずに構造や値の流れを解析します。検出した場所、外部入力との関係、解析できなかった呼出しを確認し、検出候補を実際の条件で検証します。
固定PythonコードをASTへ変換し、SQL実行の引数を調べます。公開2行だけのSQLiteで入力の影響を試し、パラメーター化したコードと比較します。
静的な検出候補と、実行で確認した挙動、未解析部分を分けて説明します。
1入力をSQLへ埋め込むコードを読む
valueをf文字列でSQLへ組み込んでいます。
from sp19 import *
print(VULNERABLE,end='')
tree=ast.parse入力をSQLへ埋め込むコードを読む(VULNERABLE)
print(type(tree.body[0]).__name__)構文木の先頭はFunctionDefです。文字列検索だけでなくPythonの構文として処理できます。
2SQL実行までの引数を調べる
教材の解析器は代入先とexecuteの第1引数を対応付けます。
print(analyzeSQL実行までの引数を調べる(VULNERABLE))3行目のsqlがdynamic_sqlです。この解析器はこの固定構文の代入を追うもので、任意の分岐や関数間解析を実装したSAST製品ではありません。
3f文字列に含まれる入力を開く
JoinedStrの構造を表示します。
node=next(n for n in ast.walk(tree) if isinstance(n,ast.JoinedStr))
print(ast.dumpf文字列に含まれる入力を開く(node,include_attributes=False))FormattedValueの中にvalueがあります。SQL構文と外部の値を連結している箇所を具体的に確認できます。
4通常の入力で実行する
固定ソースから関数を作り、alphaを検索します。
ns={};exec(VULNERABLE,ns)
db=connect()
print(ns['search通常の入力で実行する'](db,'alpha'))alphaだけが返ります。通常入力で正しく動くことだけでは、文字列構文への混入は検証できません。
5SQL構文に影響する固定入力を試す
引用符とORを含む公開試料です。
probe="' OR 1=1 --"
print(ns['searchSQL構文に影響する固定入力を試す'](db,probe))alphaとbetaが返ります。入力が値の範囲を越えてSQL条件として解釈されたことを、結果行から確認できます。
6パラメーター化したコードで再試験する
SQL文と引数を分ける実装へ置き換えます。
print(SAFE,end='')
exec(SAFE,ns)
print(analyzeパラメーター化したコードで再試験する(SAFE))
print(ns['search'](db,probe))
print(ns['search'](db,'alpha'))固定試料は0件、alphaは1件です。攻撃的な文字列も値として扱いつつ、通常の検索を維持できています。
7未知の関数呼出しを安全と決めない
外部関数でSQLを組み立てるコードを解析します。
print(WRAPPED,end='')
print(analyze未知の関数呼出しを安全と決めない(WRAPPED))unresolvedです。この解析器で追えない呼出しをconstant_sqlへ読み替えると、検出できない箇所を安全として数えてしまいます。
まとめ
構文と入力の流れを対応付けて候補を調べます。
実行結果と正常入力の回帰を照合します。
未解析を安全と扱いません。
操作履歴と解説を振り返れます。
Python 3.15 / AST・SQLite・WSGIの固定アプリ
from sp19 import *
print(VULNERABLE,end='')
tree=ast.parse(VULNERABLE)
print(type(tree.body[0]).__name__)def search(db, value):
sql = f"SELECT name FROM sample WHERE name='{value}'"
return db.execute(sql).fetchall()
FunctionDefprint(analyze(VULNERABLE))[(3, 'sql', 'dynamic_sql')]
node=next(n for n in ast.walk(tree) if isinstance(n,ast.JoinedStr))
print(ast.dump(node,include_attributes=False))JoinedStr(values=[Constant(value="SELECT name FROM sample WHERE name='"), FormattedValue(value=Name(id='value'), conversion=-1), Constant(value="'")])
ns={};exec(VULNERABLE,ns)
db=connect()
print(ns['search'](db,'alpha'))[('alpha',)]probe="' OR 1=1 --"
print(ns['search'](db,probe))[('alpha',), ('beta',)]print(SAFE,end='')
exec(SAFE,ns)
print(analyze(SAFE))
print(ns['search'](db,probe))
print(ns['search'](db,'alpha'))def search(db, value):
return db.execute("SELECT name FROM sample WHERE name=?", (value,)).fetchall()
[(2, "'SELECT name FROM sample WHERE name=?'", 'constant_sql')]
[]
[('alpha',)]print(WRAPPED,end='')
print(analyze(WRAPPED))def search(db, value):
sql = external_builder(value)
return db.execute(sql).fetchall()
[(3, 'sql', 'unresolved')]演習完了