T09 · Insecure Skill Coding Practices
- Location
scripts/doc_holmes/runner.py:43- Finding
Batch PDF discovery follows symlinks outside the authorized input directory
- Content
View full analysis
Vulnerability Details
File Location:
scripts/doc_holmes/runner.py:43-50, 150, 160-173, 290-296
Vulnerability Type: Symlink-based path escape and unauthorized document disclosure
Risk Level: MediumComplete Code Snippet
python def list_pdfs(root: str) -> list: """铁律①②:realpath 收集 + 排除目录。""" real_root = os.path.realpath(root) found = [] for dirpath, dirnames, filenames in os.walk(real_root): dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS] for name in filenames: if name.lower().endswith(".pdf"): found.append(os.path.join(dirpath, name)) return sorted(found)python pdfs = list_pdfs(input_dir)python def _one(pdf: str) -> dict: real = os.path.realpath(pdf) digest = sha256_of(real) rel = os.path.relpath(real, os.path.realpath(input_dir)) ... rep = triage_mod.triage_pdf(real) if do_triage else Nonepython eng = translate_pdf( real, file_out, base_url, api_key, model, page_count=rep.page_count if rep else None, timeout_s=per_file_timeout_s, lang_in=eff_lang_in, lang_out=lang_out, qps=qps, extra_args=extra, openai_timeout=openai_timeout)Technical Analysis
Batch discovery accepts every directory entry whose name ends in
.pdf. It does not reject symbolic links or verify that each entry's resolved path remains beneathreal_root.The worker subsequently calls
os.path.realpath(pdf), follows the symlink, and uses the resolved path insha256_of(),triage_pdf(), andtranslate_pdf(). Nocommonpathor equivalent containment check is performed after resolution.Consequently, a symlink located inside the selected batch directory can refer to a PDF outside that directory. The external file is then treated as an authorized batch input. Its content is parsed locally and passed to the translation engine, which sends document text to the user-configured OpenAI-compatible endpoint as part of the do ...[truncated 2169 chars]
- Remediation
View remediation
Remediation Suggestions
-
Resolve every candidate and enforce containment before any file operation:
python real_root = os.path.realpath(root) candidate = os.path.join(dirpath, name) resolved = os.path.realpath(candidate) try: if os.path.commonpath([real_root, resolved]) != real_root: continue except ValueError: # Different drives on Windows or otherwise incomparable paths. continue -
Reject symbolic-link inputs explicitly when links are not required:
python if os.path.islink(candidate): continue -
Apply the check again immediately before hashing, parsing, or translation to reduce time-of-check/time-of-use exposure.
-
Where supported, open files relative to a trusted directory descriptor using no-follow semantics such as
O_NOFOLLOW, then process the verified file descriptor rather than reopening an attacker-controlled pathname. -
Record rejected links and out-of-root paths in the audit log so users can diagnose skipped inputs without silently broadening the authorized scope.
-
Add regression tests covering:
- A symlinked PDF targeting a file outside the batch root.
- A symlink targeting a PDF inside the root.
- Nested symlink paths.
- Windows paths on different drives.
- Replacement of a validated entry between discovery and processing.
-
