Skip to content

feat: sparse fieldset selection with explicit allowlisting - #391

Open
LuisD-Dev wants to merge 2 commits into
Stellar-Analysis:mainfrom
LuisD-Dev:feat/sparse-fieldset-allowlist
Open

feat: sparse fieldset selection with explicit allowlisting#391
LuisD-Dev wants to merge 2 commits into
Stellar-Analysis:mainfrom
LuisD-Dev:feat/sparse-fieldset-allowlist

Conversation

@LuisD-Dev

Copy link
Copy Markdown

closes #337

Sparse Fieldset Selection with Explicit Allowlisting

Problem

REST endpoints that support ?fields=a,b,c sparse fieldsets face two
distinct risks that are easy to conflate:

  1. SQL injection — attacker-controlled input changes the structure
    of a query. Solved with parameterization.
  2. Column-leak — attacker-controlled input requests legitimately
    structured, never-intended-to-be-exposed data. This is not solvable
    with escaping, since the database has no concept of "this column
    exists but clients shouldn't be able to ask for it by name." It's only
    solvable with an explicit allowlist.

This PR addresses risk #2 by introducing a per-domain allowlist that is
deliberately separate from — and a strict subset of — each struct's full
field set.

What's included

  • backend/src/field_selection/
    • mod.rs — public API: error type, parse_fields, allowlist lookup
    • allowlist.rs — explicit per-domain allowlists (snapshots,
      aggregates), each mapped from real structs already in the codebase
      (NormalizedSnapshotSubmitted, OffChainAggregate). Each excluded
      field is documented inline with the reason it's excluded (internal
      reconciliation state, operator privacy, etc.)
    • parse.rs — parses the fields query param, rejects any
      non-allowlisted field via a typed FieldSelectionError, never
      silently drops or ignores unknown fields
    • README.md — explains why this module exists ahead of an HTTP
      server, and how to wire it in once one exists
  • backend/tests/non_allowlisted_field_test.rs — requests a real,
    known internal field that exists on the struct but was deliberately
    excluded from the allowlist; asserts rejection
  • backend/tests/injection_shaped_field_test.rs — requests a field
    name shaped like "; DROP TABLE users; --"; asserts it's rejected at
    the allowlist lookup (HashMap::getNone), not merely neutralized
    by escaping — there is no SQL layer in this codebase yet, so this test
    demonstrates the rejection path is name-validation, not sanitization
  • backend/src/lib.rs — registers the new module (+2 lines)

Design notes

  • This backend currently has no HTTP server (verified: no main.rs,
    no web framework in Cargo.toml). The module is intentionally
    transport-agnostic — pure parsing + allowlist logic, no dependency on
    axum/actix/etc. — so it can be wired into whichever HTTP layer gets
    built later without rework.
  • The client-provided field name is used only as a HashMap lookup
    key. It is never interpolated into any string that could reach a query
    or output layer.
  • Allowlists are hand-written per domain, not derived from the struct's
    #[derive] or reflection — adding a new field to a Rust struct does
    not automatically expose it via this module.

Testing

  • cargo build --all — clean, no new warnings
  • cargo test field_selection — pass
  • cargo test --test non_allowlisted_field_test — pass
  • cargo test --test injection_shaped_field_test — pass

Out of scope

  • No HTTP endpoints are wired up yet — there's nothing to wire them to.
    The README.md documents the intended integration point for when
    a server exists.
  • Unrelated to this PR: docs/ contains pre-existing case-collision
    duplicates (e.g. LICENSE.md / license.md) that cause noisy diffs
    on case-insensitive filesystems (Windows/macOS). Not touched here;
    worth a separate cleanup issue.

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.

Sparse-fieldset field selection with a per-endpoint allowlist

1 participant