Skip to content

security(score): bound and revalidate native PDF reads before allocation #864

Description

@seonghobae

Problem

read_score_pdf currently resolves the app-owned score path and then calls std::fs::read(path). The original attach path validates a non-empty regular .pdf, enforces MAX_SCORE_PDF_BYTES = 25 * 1024 * 1024, and checks the %PDF- magic, but the later read path does not repeat a bounded content check before native allocation.

This matters independently of the JavaScript response hardening in #750: a score file that is corrupted or replaced inside app-owned storage after attachment can be read into an unbounded native Vec<u8> before the JS bridge has a chance to reject an oversized response. The read path also does not re-check the %PDF- magic before returning bytes.

Exact evidence

Protected develop@acdbea6344fe1231c39535b575f4de35e4c607c9:

  • apps/desktop/core/src/lib.rs defines MAX_SCORE_PDF_BYTES and PDF_MAGIC; validate_score_pdf_source rejects empty/short/wrong-magic/oversized sources during attach.
  • apps/desktop/src-tauri/src/main.rs::read_score_pdf validates ids and path containment through resolve_existing_score_pdf, then returns std::fs::read(path) without a bounded read or content revalidation.
  • fix(score): validate PDF bridge byte arrays without coercion #750 now bounds and validates bridge response lengths, but that check occurs after the native command has already read/serialized the file and therefore cannot be the native allocation authority.

Required repair

Keep ownership in BandScope's native score-storage boundary. Prefer a GUI-independent helper in bandscope-desktop-core and call it from the Tauri command.

Acceptance criteria:

  • Read through an explicit MAX_SCORE_PDF_BYTES + 1 ceiling so file growth after metadata inspection cannot cause an unbounded native allocation.
  • Reject empty/short content and bytes that do not begin with PDF_MAGIC before returning them across IPC.
  • Preserve current project/score id allowlists and canonical app-owned path containment.
  • Keep errors path/payload-safe and do not expose absolute local paths.
  • Add realistic Rust tests for valid PDF bytes, empty/short/wrong-magic content, oversized sparse files, and the bounded-read contract.
  • Maintain 100% owned production statement/branch coverage, rustdoc/docstrings, SAST/security/SBOM/supply-chain gates, and cross-platform builds.
  • Record the residual local-filesystem TOCTOU boundary honestly; do not claim descriptor/handle-level no-follow guarantees unless actually implemented and tested.
  • Update CHANGELOG/security doctoring as appropriate.

Relationship

#750 is the canonical JavaScript bridge-response validation slice and should not be expanded by silently weakening or duplicating this native authority. This issue owns the native pre-allocation/read-time validation gap.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: apiAPI, protocol, event, or external contractarea: authAuthentication, authorization, identity, or tenant isolationarea: ci-cdCI, GitHub Actions, checks, release, or supply chainarea: securitySecurity boundary, hardening, or vulnerability preventionpriority: highHigh-priority or P1 workstatus: triagedOpen issue has an organization taxonomy assignmenttype: securitySecurity vulnerability or security-specific remediation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions