Skip to content

OAUTH-001A2K — Bound Strava OAuth token JSON responses before parsing #614

Closed

Description

@daliu

Outcome

The authorization-exchange and token-refresh Functions must fully consume successful Strava token replies through one bounded native response-body reader before UTF-8 decoding, JSON parsing, projection, or persistence. Missing or dishonest response headers must not bypass the actual streamed-byte ceiling.

Parent: #88

Split rationale

A pre-implementation runtime review split the original four-response proposal. OAuth token replies have tightly bounded accepted fields and can use one reviewable 64 KiB application ceiling. Activities may contain large provider summary fields with no published response-size guarantee, and activities/statistics also need explicit sibling-body cleanup because their fetches start together. That separate adoption will be named and reviewed after this child; it is not silently included here.

Problem

Exact main 644d9f1fe3e6c06427412fd3ae37973dbbfeca20 calls Response.json() without a body limit after confirmed HTTP success at the authorization exchange and refresh paths in functions/strava.js. A provider, intermediary, decompression expansion, or response drift can therefore make the Function buffer and parse an unbounded token reply before the existing strict field projection rejects it.

Invariant and allowed transitions

  • Use one named 65,536-byte (64 KiB) application ceiling for the content-decoded native Node 20 Response.body stream at authorization exchange and token refresh.
  • Require the exact admitted native Response, captured native headers/body getters, and a genuine readable body. Do not retain a production .json(), .text(), .arrayBuffer(), .bytes(), clone, tee, or plain-test-object fallback.
  • A missing Content-Length is allowed. If present, it must be normalized ASCII decimal digits representing a safe integer no greater than the ceiling. A malformed or declared-over-limit value fails before body reads. A lower or dishonest value never admits a body; actual streamed bytes remain authoritative.
  • Count every genuine Uint8Array chunk byte before copying. Exact limit succeeds. At byte 65,537, stop without copying the overflowing chunk and begin best-effort cancellation with no raw reason.
  • Bound reads to at most 4,096 chunks, including zero-byte chunks, so an immediately resolving zero-byte stream cannot loop forever.
  • Cancellation and lock release are guarded, never logged, never awaited indefinitely, and cannot replace the fixed endpoint result.
  • Decode only completely collected bounded bytes with fatal UTF-8 handling, then parse with captured JSON parsing. Empty/null/locked/already-used bodies, malformed chunks/read records, stream failure, malformed UTF-8/JSON, exceptional native operations, chunk overflow, and byte overflow use the existing endpoint-specific fixed unavailable result without provider details.
  • A valid bounded JSON value still passes through the current strict projector. A bounded but invalid authorization/refresh schema keeps the current endpoint-specific internal result.
  • HTTP non-success keeps its existing classification and no-body-read behavior.
  • Authorization state remains one-use before provider exchange. A token-body failure writes no secret or connection. A refresh-body failure preserves the previously stored secret, performs no CAS write, and starts no activity/statistics bearer request.

The ceiling covers content-decoded chunks consumed by this code. It does not bound headers, compressed wire bytes, Undici internal buffering, the allocation of one provider-supplied chunk before inspection, parsed-object heap amplification, CPU, or time.

Scope

  • functions/strava.js: one dependency-free bounded native JSON reader; authorization exchange and token refresh adoption only; a non-deployed testable projector seam only as required to preserve hostile-value tests.
  • functions/strava.test.js: genuine native Response/ReadableStream fixtures, trustworthy RED, boundary/integration matrix, compatibility updates, and direct preservation of the existing accessor/Proxy/custom-prototype projector cases.
  • SECURITY.md: one separately named OAUTH-001A2K current-source/residual row.

No activities/statistics response adoption, sibling-response cancellation, browser/client, callback, disconnect/revoke, Firestore schema or Rules, workflow, package, provider configuration, Firebase deployment, production data, timeout/AbortSignal policy, IAM/encryption, scope/account policy, reconciliation, or live change is included.

Trustworthy RED and required coverage

On exact base 644d9f1fe3e6c06427412fd3ae37973dbbfeca20, create a genuine successful native authorization Response with no Content-Length. Its ReadableStream must emit a valid exchange JSON document padded with trailing JSON whitespace: first 65,536 bytes, then byte 65,537. Require fixed authorization unavailable, consumed one-use state, zero secret/connection writes, no logs, and one best-effort cancellation. The old source must instead parse the complete valid document, persist both records, return success, and never cancel.

Cover:

  • one byte below, exact limit, and one byte over;
  • one-chunk and multi-chunk overflow without copying the offending chunk;
  • absent, truthful, over-limit, underreported, and exact-but-dishonest Content-Length, plus malformed/negative/unsafe values;
  • split multibyte UTF-8, BOM parity, byte-not-character counting, and malformed UTF-8;
  • empty, null, locked, and already-used body;
  • non-Uint8Array chunks, malformed reader results, stream read failure, cancellation throw/rejection/pending promise, lock-release failure, and the 4,097th zero-byte chunk;
  • a pending final chunk with no early parse, persistence, or result;
  • malformed JSON without provider bytes/errors/lengths/headers/URLs in error, logs, or test artifacts;
  • an overridden native .json() and a plain { ok: true, json } mock cannot bypass the stream boundary;
  • non-success no-body-read compatibility;
  • bounded invalid-schema internal compatibility;
  • exact-bound and ordinary bounded authorization persistence/return;
  • refresh overflow preserves the old secret/CAS state and starts no activity/statistics request;
  • ordinary bounded refresh CAS persistence;
  • frozen projection/output and fixed error compatibility.

Do not delete or weaken existing accessor, Proxy, custom-prototype, field-bound, logging, state-consumption, lifecycle, CAS, or response-status coverage. Genuine JSON bytes cannot construct every hostile JavaScript value, so preserve those projector defenses through a small reviewable direct seam that functions/index.js does not deploy.

Dependencies and residuals

A later separately named #88 child owns activities/statistics limits, their evidence-based caps, and targeted cleanup of an unread successful statistics body when activity admission fails. Request/connect/read timeouts, AbortSignal policy, compressed-wire limits, provider guarantees, lost-acknowledgement and rotated-token reconciliation, scopes/account binding, revoke/audit, IAM/encryption, deployment, configuration, and live proof also remain open under #88. Never retry a failed token response blindly: the provider may already have issued or rotated credentials.

Acceptance criteria

  • The exact-base RED is reproducible with a test-only binary diff hash and fails for the intended unbounded-parse reason.
  • Neither current successful token-response path invokes .json(), .text(), .arrayBuffer(), .bytes(), clone, or tee.
  • Exact native streamed bytes enforce the limit even with absent or false-low headers.
  • Every failure is fixed, redacted, side-effect-bounded, and endpoint-compatible.
  • Existing impossible-JSON projector defenses remain directly tested rather than silently deleted.
  • Focused checks, Functions lint/full tests, and relevant repository suites pass.
  • Dependency audits are recorded without forced upgrades.
  • Exact-head security, availability/resource-lifecycle, privacy/logging, test-quality, and backup-officer reviews find no STOP.

Officer impact: None — this server-only source guard preserves the existing member-visible success and fixed failure states while bounding OAuth token-response memory before parsing.

Officer documentation: None — no officer task, page structure, data route, permission, account ownership, deployment procedure, or visible wording changes. Existing source-only Strava guidance remains current.

Deployment evidence: At claim time, no source change, test result, merge, Firebase deployment, website publication, runmprc.com verification, Strava/provider action, production-data action, or live-behavior verification is claimed. Final evidence must record every surface separately.

Activity

  1. self-assigned this
    on Aug 2, 2026
  2. added
    priority:P0Launch blocker or urgent security risk
    type:securitySecurity or privacy boundary
    area:authAuthentication and authorization
    area:firebaseFirebase services and data
    size:MMedium multi-file issue
    status:in-progressClaimed and actively being implemented
    type:testingTest infrastructure and quality gates
    on Aug 2, 2026
  3. changed the title [-]OAUTH-001A2K — Bound Strava provider JSON responses before parsing[/-] [+]OAUTH-001A2K — Bound Strava OAuth token JSON responses before parsing[/+] on Aug 2, 2026
  4. daliu commented on Aug 2, 2026

    @daliu
    ContributorAuthor

    CLAIMED — exact scope after pre-implementation split

    Claimant: daliu / Codex
    Exact base: 644d9f1fe3e6c06427412fd3ae37973dbbfeca20
    Parent: #88

    Owned paths/outcome:

    • functions/strava.js: one bounded native JSON reader plus authorization exchange and refresh adoption only.
    • functions/strava.test.js: native byte-stream RED/matrix, endpoint integration, and preserved hostile projector tests through a non-deployed test seam.
    • SECURITY.md: one separately named OAUTH-001A2K row only.

    The earlier all-four concept was split before implementation after independent runtime review found different activity-size compatibility and sibling-body lifecycle concerns. Activities/statistics limits and sibling cancellation are not owned by this claim. Also excluded: client/callback/disconnect, officer docs, packages/workflows, Rules/schema/migration, timeouts/AbortSignal, Firebase/provider/config/data/live action.

    Required RED: exact-base native authorization Response emits valid JSON padded to 65,537 bytes without Content-Length; old source succeeds/writes and never cancels, while the fix must return fixed unavailable after the 65,536-byte ceiling, consume state once, write nothing, log nothing, and best-effort cancel.

    No source/test/deployment/live completion is claimed yet.

  5. daliu commented on Aug 2, 2026

    @daliu
    ContributorAuthor

    Trustworthy exact-base RED

    • Exact base: 644d9f1fe3e6c06427412fd3ae37973dbbfeca20.
    • Test-only binary diff SHA-256: 9e9d94b7586b04e73882f763c206e97bbe5df3821b2850c793488880256a3e72.
    • Runtime: Node v20.20.2.
    • Focused command: npm --prefix functions run test:run -- --runInBand strava.test.js -t OAUTH-001A2K.
    • Result: 1 failed, 721 skipped, 722 total.

    The genuine native successful Response has no Content-Length and streams one valid exchange JSON document padded with JSON whitespace to exactly 65,537 bytes in chunks of 65,536 plus 1. Its own test .json() override delegates to native Response.prototype.text and JSON.parse, so the old production call must consume the real body; the corrected reader must ignore this override entirely.

    Old-source witness:

    • fulfilled { ok: true, athleteId: 123456 };
    • attempted both secret and connection writes;
    • consumed the one-use OAuth state;
    • read the oversized body and canceled 0 times;
    • logged nothing.

    The required result is the existing fixed authorization unavailable, zero token/connection writes, consumed state, no logs, and exactly one best-effort cancellation at byte 65,537. No runtime or documentation change was present for this RED.

  6. daliu commented on Aug 2, 2026

    @daliu
    ContributorAuthor

    RED CORRECTION / STRENGTHENED EXACT-BASE PROOF

    Exact base: 644d9f1fe3e6c06427412fd3ae37973dbbfeca20.

    A fresh detached worktree received only one minimal integration-test patch. Binary test-patch SHA-256: 94dc7e995384a8398d918e688fb2690920d20a5d541a55a52c8ff399eec6f813.

    Node 20 focused result: 1 failed, 721 skipped, 722 total. The genuine native Response had no Content-Length; its overridden json delegated to native Response.prototype.text so the base implementation consumed streamed chunks of 65,536 and 1 bytes.

    Observed on the exact base:

    • chunkReadCount: 2
    • cancelCount: 0
    • fulfilled { ok: true, athleteId: 123456 }
    • OAuth state consumed
    • 2 connection/token writes

    Required by this child:

    • chunkReadCount: 2
    • cancelCount: 1
    • fixed unavailable authorization result
    • OAuth state remains consumed
    • 0 connection/token writes

    This supersedes the earlier RED witness by explicitly proving that both chunks were consumed. The detached worktree was removed after the run. No provider, Firebase project, production data, or live surface was contacted.

  7. daliu commented on Aug 2, 2026

    @daliu
    ContributorAuthor

    COMPLETE / CLOSED / CLAIM RELEASED

    Outcome: authorization-exchange and token-refresh replies are now bounded to 65,536 content-decoded bytes and 4,096 chunks before UTF-8 decoding, JSON parsing, strict projection, or persistence. Actual streamed bytes remain authoritative when Content-Length is absent or false-low. Failures retain fixed endpoint outcomes and the documented state/write/CAS boundaries.

    Release evidence:

    • Source head: 165934dee32ecc1daaafb596bab16160ff52e6b0; tree 3683a9ccb982b6a2bb1c67a8e0fd27a86f38bcc9; binary base-to-head diff SHA-256 0ca738c94f25e8ba7507d28e40b5fc3410f32774288afba12d0721540c1c2433.
    • Trusted RED: exact base 644d9f1fe3e6c06427412fd3ae37973dbbfeca20, minimal test-only patch SHA-256 94dc7e995384a8398d918e688fb2690920d20a5d541a55a52c8ff399eec6f813; old source read both 65,536 + 1-byte chunks, returned success, made two writes, and did not cancel.
    • Local checks: focused 45 passed; full Strava 766 passed; full Functions 7,569 passed / 64 skipped; Functions lint passed; static security/workflow suite 93 passed; frontend reviewed baseline passed; diff check passed.
    • Dependency audits unchanged with no package/lockfile change: root 59 all / 4 production; Functions 8 moderate.
    • Exact-head runtime, security/scope, resource-lifecycle, privacy/logging, test-quality, documentation, and backup-officer reviews: GO with no remaining STOP.
    • PR OAUTH-001A2K — Bound Strava OAuth token JSON responses before parsing #615 hosted CI run 30740379852: all five GitHub jobs passed. Exact-head Netlify preview 6a6f038b9701b00008066000 is ready and unpublished; page-change check was neutral because no site file changed.
    • PR OAUTH-001A2K — Bound Strava OAuth token JSON responses before parsing #615 merged at 2026-08-02T08:48:24Z as exact main b94622bd42775d84704d5fc05ee87da81f5a76e0; merge tree exactly matches the reviewed head tree.
    • Exact-main CI run 30740495924: all five jobs passed.

    Officer impact: None — the existing member-visible success and fixed failure states are preserved.

    Officer documentation: None — no officer task, page, visible wording, data route, permission, account ownership, provider configuration, or deployment procedure changed. SECURITY.md records the source/residual boundary.

    Deployment evidence:

    • Source changed: yes.
    • Tests passed: yes.
    • Code merged: yes, exact main b94622bd42775d84704d5fc05ee87da81f5a76e0.
    • Website published: no claim; no website file changed and no release workflow ran.
    • runmprc.com verified: no; no website revision or public behavior was tested.
    • Firebase deployed: no; CI only linted/tested and no release workflow ran.
    • Outside provider configured/contacted: no Strava configuration or live request.
    • Production data: not read or changed.
    • Checkout/payment/live behavior: not affected and not verified.
    • GitHub deployments for exact main: [].
    • Netlify and GitHub Pages exact-main check suites were queued with zero check runs at audit time; this is not publication evidence.

    Residuals remain exactly those named in the issue and SECURITY.md: headers/wire/Undici/one allocated chunk/heap/CPU/timeouts, exchange or rotation lost acknowledgement, activities/statistics limits and sibling cleanup, scope/account policy, reconciliation, audit, IAM/encryption, deployment, provider configuration, production data, and live proof.

    Issue #614 is complete and closed. The exact claimed paths (functions/strava.js, functions/strava.test.js, and the named SECURITY.md row) are released. The later activities/statistics child remains unclaimed and must be coordinated separately.

    COMPLETE / CLOSED / CLAIM RELEASED at b94622bd42775d84704d5fc05ee87da81f5a76e0.

  8. removed their assignment
    on Aug 2, 2026
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

    area:authAuthentication and authorizationarea:firebaseFirebase services and dataarea:stravaStrava integrationpriority:P0Launch blocker or urgent security risksize:MMedium multi-file issuetype:reliabilityReliability and recoverytype:securitySecurity or privacy boundarytype:testingTest infrastructure and quality gates

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions