Skip to content

fix(bootstrap): reject path-traversal in filer-controlled primary_doc - #214

Open
sroussey wants to merge 1 commit into
mainfrom
claude/beautiful-archimedes-r77055
Open

fix(bootstrap): reject path-traversal in filer-controlled primary_doc#214
sroussey wants to merge 1 commit into
mainfrom
claude/beautiful-archimedes-r77055

Conversation

@sroussey

Copy link
Copy Markdown
Contributor

Summary

The Feed-tarball writer composed accession-doc cache paths as
<raw>/accessiondocs/<cik10>/<accNoDash>-<primary_doc> where
primary_doc is a filer-authored field on the EDGAR submissions API.
Nothing rejected .., absolute paths, path separators, or NUL bytes,
so a hostile filer could steer both the writer
(BootstrapAccessionDocsTask.writeFiling) and the symmetric reader
(ProcessAccessionDocFormTask.readCachedDoc) anywhere on disk the
process could reach — arbitrary-file write and arbitrary-file read.

Fix

New src/util/accessionDocPath.ts exposes two named helpers used by
both paths before any composed path touches the filesystem:

  • sanitizePrimaryDoc(name) — trims and rejects an empty string, .,
    .., any / or \, NUL, or a leading /, returning the basename
    otherwise.
  • assertInsideDir(fullPath, dir) — belt-and-braces containment check
    on the resolved paths.

Writer: writeFiling sanitizes primary_doc; on rejection it logs a
warning naming the cik / accession / offending value and falls through
to the full-submission .txt fallback under the cik directory rather
than aborting the whole day. assertInsideDir runs on both the primary
and full-submission paths as a second gate.

Reader: readCachedDoc sanitizes the fileName; an unsafe name is a
silent cache miss (the caller falls back to the normal network fetch,
matching the ENOENT branch). assertInsideDir guards the composed path.

Test plan

  • New src/util/accessionDocPath.test.ts: sanitizer rejects
    ../../../etc/edgar-attacker, /etc/passwd, evil\0.htm, backslash,
    and empty; accepts and trims wf-form4.xml. assertInsideDir
    accepts a normal join and rejects a ..-escaping composed path.
  • New integration case in BootstrapAccessionDocsTask.test.ts:
    seeded filing with primary_doc: "../../../../../etc/edgar-attacker"
    writes only the full-submission .txt under the cik dir
    (docsWritten === 1), nothing lands at /etc/edgar-attacker or the
    raw root's parent, and a warning naming the filing plus the offending
    value is emitted.
  • bunx vitest run src/util/accessionDocPath.test.ts src/task/bootstrap/BootstrapAccessionDocsTask.test.ts src/task/forms/ProcessAccessionDocFormTask.cache.test.ts — 19 passed.
  • npx tsc --noEmit — clean on the touched files (the one
    pre-existing feedTarball.ts error is unrelated and present on main).

Generated by Claude Code

The Feed-tarball writer composed cache paths as
`<raw>/accessiondocs/<cik10>/<accNoDash>-<primary_doc>` where
`primary_doc` comes straight from the EDGAR submissions API — a value
under the filer's control. A hostile filer could set `primary_doc` to
something like `../../../etc/edgar-attacker` and steer the writer (and
the symmetric `readCachedDoc` reader on the forms path) anywhere the
process could reach on disk.

Add `src/util/accessionDocPath.ts` with two small helpers,
`sanitizePrimaryDoc` (rejects empty, `..`, `.`, any path separator,
NUL, or leading `/`) and `assertInsideDir` (belt-and-braces resolved-path
containment check). Both writer (`BootstrapAccessionDocsTask.writeFiling`,
which now warns and falls through to the full-submission `.txt` fallback
on a rejected name) and reader (`ProcessAccessionDocFormTask.readCachedDoc`,
which treats an unsafe name as a silent cache miss) go through them
before touching any composed path. Unit tests cover the sanitizer plus a
containment case; an integration test drives the writer end-to-end with
an attacker-controlled `primary_doc` and asserts nothing escapes the cik
directory.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants