feat(okf): scan reserved index.md/log.md in mode-b import, not path-reject (review MAJOR #2)
A received OKF bundle MAY legitimately carry index.md (directory listing, read first under progressive disclosure) and log.md (update history) at any level (spec §3.1/§6/§7). import_bundle previously hard-rejected those basenames in the T4 path gate, so a conformant third-party bundle was over-blocked in full (FAIL_SECURE) — and because the reject fired before scan_concept, index.md's body (the highest-priority injection surface) was never scanned. import_bundle now defaults allow_reserved=True: reserved basenames are scanned as structural files (path-safety checks — traversal / absolute / backslash / .md — still apply). The shadow-reject (an *upload* masquerading as index.md) is preserved: the front-end passes allow_reserved=False so a materialized upload landing on a reserved basename is still refused. That front-end opt-in was required to keep the shadow-reject once the default flipped (not in the plan's Filer set; traced from the code). - okf.py: validate_concept_path/_validate_concept/import_bundle gain the keyword; validate_concept_path default stays False (strict standalone). - tests: +3 (legit index/log admit; injection in index.md body caught; okf_version frontmatter admits). Per-concept-iteration test switched to a traversal vector; mode-b showcase's index.md surface reframed from reserved-name-reject to index.md-body-scan. - README honest-limits + CLAUDE.md context note the mode-b/upload distinction. Suite: 341 -> 344 passed. Core invariant intact (dependencies=[]).
This commit is contained in:
parent
4d53765c63
commit
0772dafb70
6 changed files with 105 additions and 18 deletions
|
|
@ -333,11 +333,12 @@ def test_import_bundle_all_clean_warns():
|
|||
|
||||
|
||||
def test_import_bundle_iterates_per_concept_not_whole_unit():
|
||||
# a reserved-name concept is rejected, but the good concept is still validated
|
||||
result = import_bundle({"index.md": _CLEAN_A, "tables/users.md": _CLEAN_B})
|
||||
# a hard-rejected concept (path traversal) is FAIL_SECURE, but the good
|
||||
# concept is still validated — iteration does not stop at the first reject.
|
||||
result = import_bundle({"../escape.md": _CLEAN_A, "tables/users.md": _CLEAN_B})
|
||||
by_path = {c.path: c for c in result.concepts}
|
||||
assert by_path["index.md"].disposition is Disposition.FAIL_SECURE
|
||||
assert by_path["index.md"].error is not None
|
||||
assert by_path["../escape.md"].disposition is Disposition.FAIL_SECURE
|
||||
assert by_path["../escape.md"].error is not None
|
||||
assert by_path["tables/users.md"].error is None
|
||||
assert by_path["tables/users.md"].disposition is Disposition.WARN
|
||||
|
||||
|
|
@ -384,6 +385,58 @@ def test_import_bundle_records_origin_channel_on_stamp():
|
|||
assert stamp.channel is Channel.MANUAL
|
||||
|
||||
|
||||
# --- A2: reserved structural files (index.md / log.md) in a received bundle ---
|
||||
# OKF spec §3.1/§6/§7: index.md (directory listing, read FIRST under progressive
|
||||
# disclosure) and log.md (update history) are legitimate structural files a
|
||||
# received bundle MAY carry at any level — not concepts, but attacker-controlled
|
||||
# text. In a mode-b import (the default), import_bundle scans their body (the
|
||||
# highest-priority injection surface) instead of path-rejecting the whole bundle.
|
||||
# The shadow-reject — an *upload* masquerading as index.md — stays in the
|
||||
# front-end/upload context (allow_reserved=False), tested in
|
||||
# test_okf_inbox_uploads.py.
|
||||
|
||||
_CLEAN_INDEX = (
|
||||
"---\ntype: table\ndescription: A directory listing.\n---\nA clean listing body.\n"
|
||||
)
|
||||
_CLEAN_LOG = "---\ntype: table\n---\nA clean change-log entry.\n"
|
||||
|
||||
|
||||
def test_legit_index_and_log_admit():
|
||||
result = import_bundle(
|
||||
{"index.md": _CLEAN_INDEX, "log.md": _CLEAN_LOG, "tables/users.md": _CLEAN_A}
|
||||
)
|
||||
by_path = {c.path: c for c in result.concepts}
|
||||
assert by_path["index.md"].error is None # scanned, not path-rejected
|
||||
assert by_path["log.md"].error is None
|
||||
assert result.disposition is Disposition.WARN # a clean structural bundle admits
|
||||
|
||||
|
||||
def test_injection_in_index_body_is_caught():
|
||||
# The coverage hole A2 closes: index.md's body was never scanned (path-rejected
|
||||
# first). Now an injection planted in the directory listing is caught.
|
||||
poisoned_index = "---\ntype: table\n---\n" + _INJECTION + "\n"
|
||||
result = import_bundle({"index.md": poisoned_index, "tables/users.md": _CLEAN_A})
|
||||
idx = {c.path: c for c in result.concepts}["index.md"]
|
||||
assert idx.error is None # scanned, not path-rejected
|
||||
assert any(f.label == "override:ignore-previous" for f in idx.report.findings)
|
||||
assert result.disposition in (Disposition.QUARANTINE_REVIEW, Disposition.FAIL_SECURE)
|
||||
|
||||
|
||||
def test_index_with_okf_version_frontmatter_admits():
|
||||
# Risk (review): okf_version frontmatter is legal only in the bundle-root
|
||||
# index.md. Scanning its body must parse the frontmatter without the strict
|
||||
# T2 gate tripping on that legitimate key.
|
||||
result = import_bundle(
|
||||
{
|
||||
"index.md": "---\nokf_version: 0.1\n---\n# Concept listing\n",
|
||||
"tables/users.md": "---\ntype: table\n---\nA clean users table.\n",
|
||||
}
|
||||
)
|
||||
by_path = {c.path: c for c in result.concepts}
|
||||
assert by_path["index.md"].error is None
|
||||
assert result.disposition is Disposition.WARN
|
||||
|
||||
|
||||
# --- T5a/A: cross-link extraction, target validation, in-import resolution ----
|
||||
# OKF links are markdown `.md` paths, bundle-absolute (`/x.md`, recommended) or
|
||||
# relative (`./x.md`); verified against SPEC.md. In-import graph only (A); the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue