Skip to content

fix(china): NBS calendar transport follows off-origin redirects and reads unbounded bodies #6262

Description

@koala73

Summary

scripts/china-macro/calendar.mjs::fetchText uses fetch() with default redirect handling and an unbounded response.text(). Its sibling transport for the same hostscripts/china-macro/source-runtime.mjs::fetchText, used by seed-china-macro.mjs — already handles both correctly (redirect: 'manual', per-hop origin re-validation, redirect cap, and a 2 MiB MAX_RESPONSE_BYTES cap). The release-calendar transport never got that hardening.

Found during the review of the NBS transient-retry fix on fix/china-release-calendar-transient-retry. Both are pre-existing and neither is caused by that change, so they were deliberately left out of its scope rather than dropped.

1. Off-origin redirect bypasses the NBS origin allowlist (content spoofing)

currentCalendarLink() enforces an origin + path allowlist, but only on the link it parses out of the fetched HTML. fetch() follows redirects automatically before that guard ever runs, so if www.stats.gov.cn returns a 302 to another origin, the response body from that other origin is parsed as NBS data and published.

Reproduced locally: a server standing in for the NBS index 302s to a second origin serving a valid-shaped calendar row.

RESULT: fetch SUCCEEDED through an off-origin redirect
  attacker-controlled events accepted: 1
  sourceUrl on those events: https://www.stats.gov.cn/english/PressRelease/ReleaseCalendar/
  reported requestCount: 1
  actual fetchFn invocations: 2
  decision status: accepted OK

Two problems visible in that output:

  • The injected event is published stamped with the trusted sourceUrl https://www.stats.gov.cn/..., so a consumer cannot tell it did not come from NBS.
  • The audited requestCount in the china_calendar_source_preflight decision records 1 while 2 HTTP hops occurred. That decision record is the official-source request accounting, so redirects silently under-report it.

Severity is bounded by requiring control of (or an open redirect on) www.stats.gov.cn, so this is not remotely exploitable today — but the allowlist exists precisely to not have to trust that, and the sibling already assumes it cannot.

Fix: mirror source-runtime.mjsredirect: 'manual', validate each Location against the permitted NBS origin/path before following, cap the redirect count, and count every hop toward requestCount.

2. Unbounded response body

return response.text(); reads the whole body with no size limit. A large (or redirected) response can consume unbounded memory and parse CPU inside a bundle section that is SIGTERM'd at 240s. source-runtime.mjs caps at MAX_RESPONSE_BYTES = 2 * 1024 * 1024.

Fix: reject an oversized Content-Length and read through a byte-limited reader, matching the sibling's cap.

Notes

  • parseNbsReleaseCalendar is a regex table parser, so a hostile body is a parser-input surface as well as a content-trust one.
  • Worth doing as one change, since both fixes land in the same ~15 lines of fetchText and the sibling supplies the exact pattern to copy.
  • A redirect change must be verified against the live endpoint first: https://www.stats.gov.cn/english/PressRelease/ReleaseCalendar/ currently answers 200 directly (no redirect), so switching to manual redirects is safe today — but confirm before shipping, since breaking this fetch re-opens the staleness incident the transient-retry fix just closed.

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