Add integrity verification for ML model artifacts before joblib.load #66
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
load_artifactsinservices/ml_inference.pycallsjoblib.loadon files from the models directory without any integrity check. joblib uses pickle internally, so a malicious.pklfile in the models directory would execute arbitrary code when the service starts or after nightly retraining.Flagged by WuMing in PR #65 (inline comment on
ml_inference.py).Threat model
An attacker needs write access to the models volume to exploit this. In a typical self-hosted deployment that level of access already implies container exec or host FS access, so the blast radius is bounded — but the risk is still worth mitigating explicitly.
Proposed fix
Option A — SHA-256 sidecar (preferred): after
save_artifactswritesmodel.pklandpipeline.pkl, write amanifest.sha256file containing their hex digests. Beforeload_artifactsdeserialises anything, recompute and compare. Detects any tampering between write and load.Option B — File permissions: document and enforce that the models directory is owned by the service user with
chmod 700. Mount the volume read-only at container start; remount rw only during the nightly retraining window.A combination of both is ideal: permissions reduce the attack surface; the sidecar provides a last-line-of-defence integrity check at deserialisation time.
Out of scope
skl2onnxbut adds significant dependency weight and TF-IDF vocabulary round-trip complexity — disproportionate for this projectExploration findings
The vulnerability is in
fenliu/training/model.py.save_artifacts(line 58–59) writesxgboost_model.pklandfeature_pipeline.pklviajoblib.dumpwith no integrity record.load_artifacts(lines 79–80) deserialises them withjoblib.loaddirectly — no check that the files are what was written.Plan
Option A — SHA-256 sidecar (implemented):
_write_manifest(output_dir)helper that iterates over the two pkl files, computes SHA-256 of each, and writesmanifest.sha256in the format<hex> <filename>(one line per file).save_artifacts._verify_manifest(output_dir)helper that readsmanifest.sha256, recomputes digests of both pkl files, and raisesValueErroron mismatch orFileNotFoundErrorif the manifest is absent.load_artifacts, before eitherjoblib.loadcall.Option B — volume permissions (documented):
Add a security note to
docs/getting-started/container-deployment.mdrecommendingchmod 700on the models directory (owned by thefenliuservice user) and, where the orchestrator supports it, mounting the volume read-only after training and switching to read-write only for the nightly retraining window.TDD order: write the three failing tests first (manifest absent, file tampered, manifest exists after save), then implement both helpers.
Key files:
packages/fenliu/src/fenliu/training/model.py,packages/fenliu/tests/test_training.py,packages/fenliu/docs/getting-started/container-deployment.md