Summary
import_apply writes raw bytes from a tar archive directly to committed artifact directories (claims/, pages/, sources/, etc.) without validating file content against the Pydantic models. A crafted bundle can inject malformed YAML or schema-violating artifacts that persist on disk and poison all subsequent list/read operations.
This is distinct from the path-traversal issue (documented in ISSUE_security_audit_findings.md Finding 1). This is about content integrity — the imported data becomes "trusted" (lives in committed directories) without ever being validated.
Location
src/vouch/bundle.py:224-239 — import_apply writes raw bytes with no schema check
src/vouch/bundle.py:166-197 — import_check only validates path-level conflicts and hash matching, not content
Buggy code
# bundle.py:224-239
for member in tar.getmembers():
if member.name == MANIFEST_NAME or not member.isfile():
continue
if member.name not in recorded:
continue
dest = kb_dir / member.name
if (
dest.exists()
and on_conflict == "skip"
and sha256_hex(dest.read_bytes()) != recorded[member.name]["sha256"]
):
skipped.append(member.name)
continue
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(tar.extractfile(member).read()) # raw bytes, no validation
written.append(member.name)
The subsequent rebuild_index (called by server.py:535 and jsonl_server.py:378) will crash on malformed YAML, but the corrupt files remain on disk.
Reproduce
import tarfile, io, json
# Craft a bundle with an invalid claim (confidence > 1.0, missing required fields)
bad_claim = b"id: bad\ntext: null\nconfidence: 999.0\n"
manifest = {
"spec": "vouch-bundle-0.1",
"bundle_id": "abc",
"files": [{"path": "claims/bad.yaml", "size": len(bad_claim),
"sha256": sha256_hex(bad_claim)}],
"counts": {"claims": 1},
"safety": {"has_proposed": False, "has_state_db": False, "has_audit_log": False},
}
with tarfile.open("evil.tar.gz", "w:gz") as tar:
info = tarfile.TarInfo("claims/bad.yaml")
info.size = len(bad_claim)
tar.addfile(info, io.BytesIO(bad_claim))
mf = json.dumps(manifest).encode()
info = tarfile.TarInfo("manifest.json")
info.size = len(mf)
tar.addfile(info, io.BytesIO(mf))
# import_check passes (no content validation)
# import_apply writes claims/bad.yaml to disk
# Every subsequent list_claims() call raises a Pydantic ValidationError
Reach
Any vouch import-apply or kb.import_apply call. The corrupted files persist across restarts and poison all list_claims(), list_pages(), etc. calls — a single bad file in claims/ makes the entire claim listing fail.
A crafted bundle can also inject:
- Claims with
confidence: 999 (bypasses ge=0.0, le=1.0 validators)
- Sources with mismatched
id vs actual content hash (breaks content-addressing invariant)
- Valid YAML but with fields that don't match required Pydantic types
Recommended fix
After extracting each file, validate it against the appropriate Pydantic model based on subdirectory before writing:
# bundle.py — add after reading bytes, before writing
from .models import Claim, Page, Entity, Relation, Evidence, Session, Source, Proposal
VALIDATORS = {
"claims": lambda data: Claim.model_validate(yaml.safe_load(data)),
"pages": lambda data: _deserialize_page(data.decode()),
"entities": lambda data: Entity.model_validate(yaml.safe_load(data)),
"relations": lambda data: Relation.model_validate(yaml.safe_load(data)),
"evidence": lambda data: Evidence.model_validate(yaml.safe_load(data)),
"sessions": lambda data: Session.model_validate(yaml.safe_load(data)),
"decided": lambda data: Proposal.model_validate(yaml.safe_load(data)),
}
body = tar.extractfile(member).read()
subdir = member.name.split("/")[0]
if subdir in VALIDATORS:
try:
VALIDATORS[subdir](body)
except Exception as e:
issues.append(f"schema validation failed: {member.name}: {e}")
continue
dest.write_bytes(body)
Ideally, also add content validation to import_check so issues surface before import_apply is called.
Summary
import_applywrites raw bytes from a tar archive directly to committed artifact directories (claims/,pages/,sources/, etc.) without validating file content against the Pydantic models. A crafted bundle can inject malformed YAML or schema-violating artifacts that persist on disk and poison all subsequent list/read operations.This is distinct from the path-traversal issue (documented in
ISSUE_security_audit_findings.mdFinding 1). This is about content integrity — the imported data becomes "trusted" (lives in committed directories) without ever being validated.Location
src/vouch/bundle.py:224-239—import_applywrites raw bytes with no schema checksrc/vouch/bundle.py:166-197—import_checkonly validates path-level conflicts and hash matching, not contentBuggy code
The subsequent
rebuild_index(called byserver.py:535andjsonl_server.py:378) will crash on malformed YAML, but the corrupt files remain on disk.Reproduce
Reach
Any
vouch import-applyorkb.import_applycall. The corrupted files persist across restarts and poison alllist_claims(),list_pages(), etc. calls — a single bad file inclaims/makes the entire claim listing fail.A crafted bundle can also inject:
confidence: 999(bypassesge=0.0, le=1.0validators)idvs actual content hash (breaks content-addressing invariant)Recommended fix
After extracting each file, validate it against the appropriate Pydantic model based on subdirectory before writing:
Ideally, also add content validation to
import_checkso issues surface beforeimport_applyis called.