Skip to content

RST include fan-out is exponential; model and ONNX Runtime downloads are unverified #109

Description

@citron07r

Companion to #108, same disclosure note: no SECURITY.md and private reporting is off, so this is public. Fix-first, no recipe.

1. RST .. include:: fan-out is exponential

crates/vera-core/src/parsing/sphinx.rs:52-117 has a depth cap of 16 (:16, :60), a cycle guard (:81), and a genuinely correct containment check at :130-143. What it lacks is a fan-out bound.

The cycle guard tests only the current stack, so a DAG is allowed: file A may include B many times, and each expansion re-runs the recursion independently. include_cache (:84-92) caches raw bytes but never the expansion, and nothing caps the length of output (:64, :111). Output size is O(fanout^depth).

Measured: 9 files, fan-out 3, depth 8, 44 KB on disk, killed at 2m37s holding 2,350,800 KB RSS. Roughly 53,000x amplification, no termination.

Two amplifiers:

  • std::fs::read at sphinx.rs:87 reads the include target with no size cap, so including a large in-repo file bypasses the 1 MB max_file_size_bytes gate entirely.
  • preprocess_rst is also on the hashing path (indexing/update.rs:129, via indexing/freshness.rs), so the staleness check makes vera search and vera update hit the same bomb. There is no way to search such a repository without triggering it.

Suggested fix: thread an output-byte budget through resolve_includes_recursive and stop when exceeded, plus a size cap on the fs::read. Caching the expansion rather than the raw bytes would also collapse the DAG case.

2. Downloaded models and the ONNX Runtime library are unverified

ensure_model_file_impl (local_models/assets.rs:359-459) fetches from .../resolve/main/... (:393).

  • No revision pinning. resolve/main tracks a mutable branch, so the content can change under a fixed config.
  • No integrity check. The only gate is validate_file (:254-267): non-empty, regular file, plus a 4 KB protobuf header sniff for ONNX (:269-320) that succeeds on any well-formed graph. There is no sha256/digest/etag handling anywhere under local_models/. Hugging Face returns the sha256 in X-Linked-Etag and it is not consulted.
  • The result goes to Session::commit_from_file (embedding/local_provider.rs:861, retrieval/local_reranker.rs:263), and ONNX protobuf parsing is a known attack surface.

The same applies to the native library: ort.rs:366-417 downloads a release archive and extracts it, and ensure_ort_runtime (ort.rs:20-40) dlopens the result. No checksum. This is not confined to vera setup: ensure_ort_library_for_ep is called lazily from local_provider.rs:301 and local_reranker.rs:23, so an ordinary vera search on a fresh machine downloads and loads unverified native code.

Context rather than a code bug: the default embedding repo is a third-party personal account rather than the model author or onnx-community, which makes account takeover a live path. Pinning to a commit SHA and recording expected digests closes both that and the silent-swap case cheaply.

TLS itself is fine: rustls with ring, and no danger_accept_invalid_certs anywhere.

3. git runs inside the untrusted repo with its config honoured

git_scope.rs:95-118. Argument handling is correct and worth stating: no shell, validate_revision (:44-50) rejects leading -, and -- separators are present. There is no argument injection.

The residual is current_dir(repo_root) at :98 with an unfiltered environment, so git reads the repository's own .git/config. core.fsmonitor names a program git executes during git diff and git ls-files --others, which is exactly what --changed runs at :60-70. safe.directory does not help, since a repo you cloned yourself is owned by you.

Inherited from git rather than introduced here, and cheap to mitigate: prepend -c core.fsmonitor= -c core.hooksPath=/dev/null in run_git.

4. API keys are in the ambient environment for every child process

vera-cli/src/state.rs:208 and :216 copy embedding_api_key and reranker_api_key from credentials.json into the process environment at startup (main.rs:37). Every child inherits them: git in an untrusted repo (item 3), sh -c at commands/setup.rs:356, powershell at ort.rs:1294, and pip install at ort.rs:105, which runs arbitrary package code from PyPI with the user's inference credentials in its environment.

Passing the keys to the providers explicitly instead of via the environment removes this.

vera doctor is clean here: check_env_group (commands/doctor.rs:237-254) reports only a count, never a value.

Also checked, and correct

Model path traversal is properly rejected (assets.rs:461-471, with tests). Archive extraction has no zip-slip or tar-slip. write_private_file (state.rs:255-286) does 0o600, sync_all, re-apply, atomic rename. vera serve binds loopback by default and its bearer comparison is constant-time. SQL is fully parameterized; the only format!-built SQL interpolates integers from config. All eight unsafe blocks are sound.

Verified on master at e3d79b3. Items 1 and 4 were reproduced by execution; 2 and 3 are by code reading.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions