一覧SD-14-1 · 依存・ビルド・配布の防御

依存パッケージの検証と脆弱性管理

構成の同定と影響の調査を分け、確認できた範囲に沿って更新対象を決めます。

説明

依存ライブラリを調べるときは、名前だけでなく取得元、版、実際のアーカイブを対応付けます。構成一覧に載っていることと、問題のある処理がアプリから呼ばれることも別の情報です。

教材用example-parserの固定アーカイブと構成表を使います。ハッシュ、依存関係、仮のsample-advisory、呼出し経路を順に照合し、更新後の版まで追います。

構成の同定と影響の調査を分け、確認できた範囲に沿って更新対象を決めます。

1名前が同じでも取得元が異なる構成を読む

p1とp2は同名・同版ですが取得元が違う試料です。advisoryも教材用の仮の資料です。

from sd14 import * db = inventory() print(db.execute("SELECT * FROM packages取得元を含む構成情報を読む ORDER BY id").fetchall())

安定IDで取得元まで識別します。パッケージ名だけを結合キーにすると、別の対象へ情報を誤って付ける場合があります。

2固定アーカイブの中身とダイジェストを確認する

版1.0.0を持つ小さなtarを作り、そのバイト列を固定値として扱います。

data = artifact("1.0.0") locked = hashlib.sha256実アーカイブの同一性を計算する(data).hexdigest() with tarfile.open(fileobj=io.BytesIO(data)) as tar: print(tar.getnames()) print(tar.extractfile展開実行せず内部の版を読む("package/version.txt").read()) print(locked)

アーカイブの版とSHA-256を確認できます。ハッシュは照合する固定値を信頼できる場所で管理して初めて、差替え検出に使えます。

3一バイト変わったアーカイブを検出する

受領データの末尾1バイトを変えて、固定した値と比較します。

changed = data[:-1]+bytes([data[-1]^1固定資料の1ビットを変える]) print(hashlib.sha256(changed).hexdigest()==locked) print(hashlib.sha256(data).hexdigest()==locked)

変更版はFalse、元はTrueです。版名が同じでも、バイト列が変われば検出できます。

4実際に採用している依存へ情報を結合する

web-appが使うのはp1です。

print(db.execute("SELECT p.name,p.version,p.source,n.id FROM edges e JOIN packages採用中の安定IDへ結合する p ON p.id=e.child LEFT JOIN notices n ON n.package=p.id AND n.version=p.version WHERE e.parent='p3'").fetchall())

sample-advisoryはp1にだけ対応します。同名のp2や、採用していない構成を混ぜずに調査します。

5問題の入口が呼ばれるか経路を調べる

仮資料の対象入口はlegacy_decodeです。現在の呼出し図はrequest→parse→decodeです。

print(db.execute("SELECT * FROM calls").fetchall()) print(db.execute("WITH RECURSIVE入口から辿れる関数を列挙する path(fn) AS (SELECT 'request' UNION SELECT c.callee FROM calls c JOIN path p ON c.caller=p.fn) SELECT fn FROM path").fetchall())

現在の固定図にはlegacy_decodeがありません。ただし、動的呼出しや未収集経路があれば、この図だけで到達不能を断定できません。

6呼出しを追加すると評価が変わることを確認する

追加機能がlegacy_decodeを呼ぶケースを反映します。

db.execute("INSERT INTO calls新機能の呼出し辺を加える VALUES('parse','legacy_decode')") print(db.execute("WITH RECURSIVE path(fn) AS (SELECT 'request' UNION SELECT c.callee FROM calls c JOIN path p ON c.caller=p.fn) SELECT fn FROM path WHERE fn='legacy_decode'").fetchall())

対象入口が列挙されます。依存の版が変わらなくても、呼出し方の変更で調査結果が変わり得ます。

7更新版の同一性と構成情報を更新する

版1.0.1は、この仮資料の影響版1.0.0とは異なります。

updated = artifact("1.0.1")更新版アーカイブを作る print(hashlib.sha256(updated).hexdigest()!=locked) db.execute("UPDATE packages実際に採用した版を記録する SET version='1.0.1' WHERE id='p1'") print(db.execute("SELECT p.id,n.id FROM packages p LEFT JOIN notices n ON n.package=p.id AND n.version=p.version WHERE p.id='p1'").fetchall())

ハッシュが変わり、この仮資料との版一致はなくなります。実運用では公式の影響範囲、修正情報、アプリの回帰結果も併せて確認します。

まとめ

名前・版・取得元・実バイト列を対応付けます。

構成の存在と問題処理への到達を分けて調べます。

更新後は構成表と回帰検査を更新します。

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

Python 3.15 / 固定の公開試料

1
from sd14 import * db = inventory() print(db.execute("SELECT * FROM packages ORDER BY id").fetchall())
[('p1', 'example-parser', '1.0.0', 'https://packages.example.test'), ('p2', 'example-parser', '1.0.0', 'https://other.example.test'), ('p3', 'web-app', '2.0.0', 'local')]
2
data = artifact("1.0.0") locked = hashlib.sha256(data).hexdigest() with tarfile.open(fileobj=io.BytesIO(data)) as tar: print(tar.getnames()) print(tar.extractfile("package/version.txt").read()) print(locked)
['package/version.txt', 'package/parser.py']
b'1.0.0'
e849b95b8a7bd0d18300a3443aafb63fb743b0676ee1f68d314753d60c12f550
3
changed = data[:-1]+bytes([data[-1]^1]) print(hashlib.sha256(changed).hexdigest()==locked) print(hashlib.sha256(data).hexdigest()==locked)
False
True
4
print(db.execute("SELECT p.name,p.version,p.source,n.id FROM edges e JOIN packages p ON p.id=e.child LEFT JOIN notices n ON n.package=p.id AND n.version=p.version WHERE e.parent='p3'").fetchall())
[('example-parser', '1.0.0', 'https://packages.example.test', 'sample-advisory')]
5
print(db.execute("SELECT * FROM calls").fetchall()) print(db.execute("WITH RECURSIVE path(fn) AS (SELECT 'request' UNION SELECT c.callee FROM calls c JOIN path p ON c.caller=p.fn) SELECT fn FROM path").fetchall())
[('request', 'parse'), ('parse', 'decode')]
[('request',), ('parse',), ('decode',)]
6
db.execute("INSERT INTO calls VALUES('parse','legacy_decode')") print(db.execute("WITH RECURSIVE path(fn) AS (SELECT 'request' UNION SELECT c.callee FROM calls c JOIN path p ON c.caller=p.fn) SELECT fn FROM path WHERE fn='legacy_decode'").fetchall())
[('legacy_decode',)]
7
updated = artifact("1.0.1") print(hashlib.sha256(updated).hexdigest()!=locked) db.execute("UPDATE packages SET version='1.0.1' WHERE id='p1'") print(db.execute("SELECT p.id,n.id FROM packages p LEFT JOIN notices n ON n.package=p.id AND n.version=p.version WHERE p.id='p1'").fetchall())
True
[('p1', None)]

演習完了