一覧SP-19-1 · AppSec製品:コード・依存関係・動作の検査

静的解析による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の固定アプリ

1
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()
FunctionDef
2
print(analyze(VULNERABLE))
[(3, 'sql', 'dynamic_sql')]
3
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="'")])
4
ns={};exec(VULNERABLE,ns) db=connect() print(ns['search'](db,'alpha'))
[('alpha',)]
5
probe="' OR 1=1 --" print(ns['search'](db,probe))
[('alpha',), ('beta',)]
6
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',)]
7
print(WRAPPED,end='') print(analyze(WRAPPED))
def search(db, value):
    sql = external_builder(value)
    return db.execute(sql).fetchall()
[(3, 'sql', 'unresolved')]

演習完了