Summary
scripts/china-macro/calendar.mjs::fetchText uses fetch() with default redirect handling and an unbounded response.text(). Its sibling transport for the same host — scripts/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.mjs — redirect: '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.
Summary
scripts/china-macro/calendar.mjs::fetchTextusesfetch()with default redirect handling and an unboundedresponse.text(). Its sibling transport for the same host —scripts/china-macro/source-runtime.mjs::fetchText, used byseed-china-macro.mjs— already handles both correctly (redirect: 'manual', per-hop origin re-validation, redirect cap, and a 2 MiBMAX_RESPONSE_BYTEScap). 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 ifwww.stats.gov.cnreturns 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.
Two problems visible in that output:
sourceUrlhttps://www.stats.gov.cn/..., so a consumer cannot tell it did not come from NBS.requestCountin thechina_calendar_source_preflightdecision 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.mjs—redirect: 'manual', validate eachLocationagainst the permitted NBS origin/path before following, cap the redirect count, and count every hop towardrequestCount.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.mjscaps atMAX_RESPONSE_BYTES = 2 * 1024 * 1024.Fix: reject an oversized
Content-Lengthand read through a byte-limited reader, matching the sibling's cap.Notes
parseNbsReleaseCalendaris a regex table parser, so a hostile body is a parser-input surface as well as a content-trust one.fetchTextand the sibling supplies the exact pattern to copy.https://www.stats.gov.cn/english/PressRelease/ReleaseCalendar/currently answers200directly (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.