Repository navigation
Commit 6209964
chore(osv): exempt sprintf-js GHSA-hp3w-g68c-fv3c until 2026-11-05 (no fixed release exists) (#21952)
Part of #21945
Clause-②: no
## What this does
This PR adds one `[[IgnoredVulns]]` entry to `osv-scanner.toml`, for
**GHSA-hp3w-g68c-fv3c** (`sprintf-js` 1.1.3, DoS through unbounded
precision specifiers, CVSS 6.9). It is the only one of `main`'s four OSV
advisories with no fixed release. The ledger header's convention 3 says
an exemption lands in its own `osv-exemption`-labelled PR, so this PR
carries that entry and nothing else. The other three advisories are
fixed in #21951 (lockfile re-lock plus a `katex` override).
**Which half this leaves:** the three fixable advisories, which are
#21951's. This PR alone does not turn the OSV step green, and neither
does #21951 alone. The card stays open when this PR merges, and the PM
finishes it after both have landed.
```toml
[[IgnoredVulns]]
id = "GHSA-hp3w-g68c-fv3c"
ignoreUntil = 2026-11-05
reason = "GHSA-hp3w-g68c-fv3c — no fixed sprintf-js exists (1.1.3, the latest release, is the last affected version), and it arrives only transitively through tedious 18.6.2 (driver-sql's optional mssql peer) and fengari 0.1.5 (under ioredis-mock, a service-cluster-redis devDependency), whose latest releases (tedious 20.3.3, fengari 0.1.5) still require ^1.1.3; remove when sprintf-js publishes a fix or both parents drop it."
```
The entry follows the header's conventions:
- **Convention 1:** `ignoreUntil` is a bare TOML date, 30 days out,
which is the default and well under the 90-day ceiling.
- **Convention 2:** `reason` is the advisory URL, an em dash, then one
sentence on why the advisory cannot be fixed right now.
- **One id only:** the scanner applies an exemption to the advisory's
aliases too, so the CVE alias (CVE-2026-97058) gets no entry of its own.
## Exemption decision
### There is no fix to take
| Fact | Reading |
|---|---|
| Advisory range (OSV offline npm DB, downloaded 2026-10-06) |
`introduced: 0` up to `last_affected: 1.1.3`, so there is no `fixed`
event |
| `npm view sprintf-js version` | `1.1.3` (published 2023-09-11, the
latest) |
| `tedious` (latest 20.3.3, `next` 20.3.4) | every major checked
(18.6.2, 19.0.0, 19.2.1, 20.0.0, 20.3.3, 20.3.4) declares `sprintf-js
^1.1.3` |
| `fengari` (latest 0.1.5) | declares `sprintf-js ^1.1.3` |
| `pnpm why -r sprintf-js` | one copy, 1.1.3; the only parents are
`fengari@0.1.5` and `tedious@18.6.2` |
No override can help, because there is no version to override to. Moving
either parent to a newer release changes nothing either.
### Where each parent is used in this workspace
- **`tedious` 18.6.2** is the MSSQL driver. `@objectstack/driver-sql`
declares it as an **optional peer**
(`packages/drivers/driver-sql/package.json:35`, with `optional: true` at
`:44`). The workspace installs a project's own peers (`.npmrc`
`auto-install-peers=true`), and `knex@3.3.0` picks it up as its optional
mssql peer, both under `driver-sql` and under `driver-sqlite-wasm`.
- In this repository it is reached only by driver-sql tests that build a
`client: 'mssql'` driver without a server:
`sql-driver-date-bucket.test.ts:163`,
`sql-driver-text-case-conformance.test.ts:366`,
`sql-driver-20446-empty-flip.test.ts:168` and
`sql-driver-20987-json-membership-move.test.ts:163`.
- The CLI's bundler keeps it external
(`packages/cli/src/utils/config.ts:95`).
- A downstream install that uses mssql brings its own `tedious`, and so
its own `sprintf-js`. This entry does not reach that install, and
nothing here can.
- **`fengari` 0.1.5** is a Lua VM in JavaScript. It sits under
`ioredis-mock` 8.13.1, directly and through `fengari-interop`, and
`ioredis-mock` is a **devDependency** of
`@objectstack/service-cluster-redis` (`package.json:33`).
- It runs only in that package's tests, where `ioredis-mock` executes
the package's two Lua scripts. Both are constants: `RELEASE_SCRIPT` at
`src/lock.ts:18` and `RENEW_SCRIPT` at `src/lock.ts:30`, sent by the
`eval` calls at `src/lock.ts:163` and `:186`.
- In production, `ioredis` sends `EVAL` to a real Redis, and `fengari`
is never loaded. The published package depends on `ioredis` only.
### Does either path pass an untrusted format string to `sprintf`?
The advisory's precondition is an attacker who controls the **format
string**, so that a precision specifier outside what `toFixed` /
`toExponential` / `toPrecision` accept throws a `RangeError`.
- **tedious: no.** All 12 `sprintf` calls in `tedious@18.6.2/lib` take a
string-literal format, and data only ever enters as an argument:
- `value-parser.js:409` and `:485`;
- `metadata-parser.js:104` and `:355`;
- `prelogin-payload.js:204`;
- `login7-payload.js:397` (four calls);
- `packet.js:131`, `:144` and `:155`.
- **fengari: only from Lua source, and only partly.** The Lua script
supplies the format.
- `string.format` (`fengari/src/lstrlib.js:334`) passes the script's own
format to `sprintf` at `:361`, `:373` and `:391`.
- It does so only after `scanformat` (`:301`) caps width and precision
at two digits and raises `invalid format (width or precision too long)`
(`:314`). So the advisory's over-100 precision never reaches `sprintf`.
Measured: `string.format("%.100f", 1.5)` is a Lua error, and `"%.99f"`
formats normally.
- The lower edge, `"%.0g"` (`toPrecision(0)`), does still reach
`sprintf`. It fails the Lua call (measured: non-zero status).
- The other three `sprintf` calls in that file (`:172`, `:188` and
`:285`) use constant formats.
- Exposure therefore needs an attacker who can write the Lua source that
`ioredis-mock` runs. In this workspace the only Lua source is the two
constant scripts above, in tests.
**Net:** this repository passes no untrusted format string to `sprintf`
on any path.
### Routes considered and not taken
- **Drop `tedious` from `driver-sql`'s optional peers.** This removes a
published peer declaration, which is a contract change for MSSQL users.
The four tests above build an mssql client, and their own comments say
knex resolves `tedious` from this workspace, so they would need a
different stand-in client (not measured here). It would also not clear
`fengari`. That is outside this card.
- **Replace `ioredis-mock`.** That would mean a test-double migration
for `service-cluster-redis`, whose version pair is pinned in
`src/ioredis-pair.pin.test.ts`. It would not clear `tedious`. Also
outside this card.
Both parents would have to go for the advisory to leave the lockfile, so
neither route is a fix on its own.
### Renewal trigger
`ignoreUntil = 2026-11-05`. After that date the scanner stops filtering
the advisory and the step goes red on its own. **Remove the entry when
`sprintf-js` publishes a fix, or when both `tedious` and `fengari` drop
it.** Removing it can ride along with that fix, while a renewal is a new
decision and needs its own `osv-exemption` PR.
## OSV reading at this head (`e6a3636f`)
Measured locally with OSV-Scanner **v2.3.8**, the version
`validate-deps.yml` pins. `api.osv.dev` answers 403 from this container,
so the scan ran in offline mode against the OSV npm database the scanner
downloaded on 2026-10-06. Exit 1, with `sprintf-js` filtered and three
rows left, which #21951 fixes:
```text
GHSA-hp3w-g68c-fv3c and 1 alias have been filtered out because: GHSA-hp3w-g68c-fv3c — no fixed sprintf-js exists …
Filtered 1 vulnerability from output
| https://osv.dev/GHSA-238p-pmpm-9mq7 | 2.1 | npm | katex | 0.16.47 | 0.18.2 | pnpm-lock.yaml |
| https://osv.dev/GHSA-jqcg-44mw-7w3h | 9.1 | npm | proxy-addr | 2.0.7 | 2.0.8 | pnpm-lock.yaml |
| https://osv.dev/GHSA-68fv-2mgg-jv7q | 8.7 | npm | source-map-js | 1.2.1 | 1.2.2 | pnpm-lock.yaml |
```
This ledger scanned with #21951's lockfile: exit 0, `No issues found`.
## Changeset
`skip-changeset`. `osv-scanner.toml` is repo-root configuration and
ships in no package's `files[]`.
## Local verification, at `e6a3636f`
The gates come from `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at this head: 13 commands. The
`--ran` reconciliation, with exit codes recorded before any pipe, is
**13 derived, 13 run, 0 NOT-MEASURED, 0 UNRUN**:
- `node scripts/check-changeset-fixed.mjs`: 0.
- `node scripts/check-closing-keyword-parity.mjs` (+ `--self-test`): 0 /
0.
- `node scripts/check-comment-mask-corpus.mjs`: 0.
- `node scripts/check-osv-exemptions.mjs` (+ `--self-test`): 0 / 0. The
verdict line reads `✓ 1 OSV exemption(s) in osv-scanner.toml: all carry
an unexpired ignoreUntil within 90 days and a reason with an advisory
link.`
- `pnpm check:driver-memory-census` · `check:gitlink-declared` ·
`check:nul-bytes` · `check:override-consistency` ·
`check:refd-timer-probe` · `check:vendor-export-contract-resolve` ·
`check:watch-hint-literal`: 0 each.
## Acceptance notes
- The ledger's header still says "This ledger currently holds ZERO
exemptions." Once this entry lands, that sentence is no longer true.
This PR leaves the header byte-identical so that its diff is the entry
alone. If the reviewer wants it, the one-line count edit can go into
this same PR.
- The `osv-exemption` label did not exist in this repository before this
PR: `GET /labels/osv-exemption` answered 404. No exemption has landed
since the convention was written. The additive label write on this PR
creates it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 01e0f71 commit 6209964
1 file changed
Lines changed: 8 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
12 | | - | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
| |||
83 | 84 | | |
84 | 85 | | |
85 | 86 | | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
0 commit comments