CVE-2026-48816: Verified Repro With Script Download
CVE-2026-48816: sigstore-js Insufficient Verification of Data Authenticity
CVE-2026-48816 is verified against sigstore/sigstore-js · github. Fixed in 3.1.1. This medium reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00210.
What Is CVE-2026-48816?
CVE-2026-48816 (GHSA-xgjw-pm74-86q4) is a medium-severity insufficient verification of data authenticity (CWE-345) in @sigstore/verify, where a transparency-log timestamp used for certificate validity and timestampThreshold checks is not cryptographically bound in inclusionProof-only bundle entries. Pruva reproduced it (reproduction REPRO-2026-00210).
CVE-2026-48816 Severity & CVSS Score
CVE-2026-48816 is rated medium severity, with a CVSS base score of 6.5 out of 10.
Medium — meaningful risk under specific conditions. Schedule a fix in the normal cycle.
How to Reproduce CVE-2026-48816
pruva-verify REPRO-2026-00210 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00210/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-48816
- reached the target end-to-end
- high confidence
- the upstream fix blocks the same trigger
tlogEntries[].integratedTime in an attacker-supplied sigstore bundle (inclusionProof-only entry, no signed inclusionPromise)
- Verifier.verify
- verifyTimestamps
- getTLogTimestamp (packages/verify/src/timestamp/index.ts) treats unauthenticated integratedTime as a trusted timestamp; verifySigningKey/verifyCertificate (packages/verify/src/key/index.ts) accepts an expired certificate at the attacker-chosen time
reproduction_steps.sh Bypass of the @sigstore/verify 3.1.1 fix (commit f074710) for CVE-2026-48816. The fix gates getTLogTimestamp() on inclusionPromise *presence* and relies on verifyTLogs()->verifyTLogSET() to authenticate integratedTime. A SignedEntity supplied to the public Verifier.verify(SignedEntity) API in which the timestamp-provi…
How the agent worked
Root Cause and Exploit Chain for CVE-2026-48816
@sigstore/verify (the sigstore-js verification library) derives a
transparency-log timestamp directly from tlogEntries[].integratedTime and
uses that timestamp both to check the Fulcio signing certificate's validity
window and to satisfy the timestampThreshold. For a bundle whose tlog entry
is inclusionProof-only (it carries an inclusionProof but no signed
inclusionPromise/SET), integratedTime is not cryptographically bound:
the inclusion-proof path (verifyCheckpoint + verifyMerkleInclusion) proves
Merkle-tree inclusion against a signed checkpoint but never binds the
integratedTime value, whereas only the signed inclusionPromise/SET path
(verifyTLogSET) signs over integratedTime. As a result, an attacker who can
supply an untrusted bundle can choose integratedTime freely and thereby
influence time-based verification decisions — in particular making an
expired certificate appear to have been valid at signing time.
- Package/component affected:
@sigstore/verify(npm), part of thesigstore/sigstore-jsmonorepo. Affected source files:packages/verify/src/bundle/index.ts—toSignedEntityadds atransparency-logtimestamp for every tlog entry whereintegratedTime != '0', regardless of whether aninclusionPromiseis present.packages/verify/src/timestamp/index.ts—getTLogTimestampconvertsentry.integratedTimeinto aDatewith no check that the value is cryptographically bound.packages/verify/src/verifier.ts—verifyTimestampscounts every transparency-log timestamp towardtimestampThreshold, andverify()runs timestamp (and therefore certificate-validity) checks beforeverifyTLogs(inclusion proof) — so the unauthenticated time is consumed before any inclusion check that could constrain it.packages/verify/src/tlog/index.ts+packages/verify/src/tlog/set.ts— only theinclusionPromise/SET path (verifyTLogSET) signs overintegratedTime; theinclusionProofpath does not.
- Affected versions:
@sigstore/verify3.1.0 (vulnerable). Fixed in 3.1.1.- Vulnerable commit (anchored to the fix's parent):
7845532(f074710^, "OID certificate extension verification (#1658)", still shipping@sigstore/verify3.1.0). - Fixed commit:
f074710("reject integratedTime w/o inclusionPromise (#1659)"), released as 3.1.1 viac1dc7d4("Version Packages (#1607)").
- Vulnerable commit (anchored to the fix's parent):
- Risk level / consequences: Medium (advisory CVSS 6.5, CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N; CWE-345 Insufficient Verification of Data Authenticity). Integrity impact: a consumer that accepts attacker-provided bundle inputs and relies on tlog-derived timestamps for certificate-validity checks can be tricked into accepting a bundle whose signing certificate is expired (or otherwise time-invalid) by an unauthenticated timestamp value chosen by the attacker.
Impact Parity
- Disclosed/claimed maximum impact: Integrity bypass — an attacker-supplied
bundle can influence time-based verification (certificate validity windows and
timestampThreshold) via an unauthenticatedintegratedTimein an inclusionProof-only tlog entry. - Reproduced impact from this run:
- The real
getTLogTimestamp()callsite accepts an inclusionProof-only entry'sintegratedTimeas a trusted timestamp (vulnerable) and returnsundefinedfor it (fixed). - The real
Verifier.verify()succeeds on a real cert-signed bundle mutated to be inclusionProof-only with an attacker-chosenintegratedTime, accepting a certificate that is expired now (notAfter = 2025-12-14T02:14:39Z,now = 2026-07-02…) because the unauthenticatedintegratedTimeis set inside the cert's validity window (notBefore = 2025-12-14T02:04:39Z). The fixed build rejects the identical bundle withTIMESTAMP_ERROR.
- The real
- Parity:
full— the disclosed trust gap (unauthenticatedintegratedTimeused for certificate validity /timestampThreshold) is demonstrated through the real@sigstore/verifyverification path with a concrete expired-certificate-accepted outcome and a fixed-version negative control. - Not demonstrated: No code execution / memory corruption is claimed or demonstrated; the impact is a verification/authenticity bypass (CWE-345).
Root Cause
toSignedEntity (bundle/index.ts) turns every tlog entry with
integratedTime != '0' into a transparency-log timestamp. verifyTimestamps
(verifier.ts) then calls getTLogTimestamp(entry) (timestamp/index.ts) which,
in the vulnerable code, unconditionally returns:
return {
type: 'transparency-log',
logID: entry.logId.keyId,
timestamp: new Date(Number(entry.integratedTime) * 1000),
};
with no check that entry.inclusionPromise exists. That result is counted
toward timestampThreshold and is returned to verifySigningKey, which calls
verifyCertificate(cert, timestamps, trustMaterial) →
verifyCertificateChain(timestamp, leaf, CAs) — i.e. the certificate's
validity window is checked at the attacker-chosen integratedTime, not at
the current time and not at a cryptographically-bound time. Critically,
verify() performs verifyTimestamps (and thus the certificate-validity
decision) before verifyTLogs (the inclusion-proof check), so the
unauthenticated time is consumed before any check that could constrain it.
Only the inclusionPromise/SET path (verifyTLogSET in tlog/set.ts) signs
over integratedTime (body, integratedTime, logIndex, logID). The
inclusionProof path (verifyCheckpoint + verifyMerkleInclusion) verifies
Merkle inclusion against a signed checkpoint but does not bind
integratedTime. Therefore an inclusionProof-only entry can pass inclusion
verification while leaving integratedTime fully attacker-controlled.
Fix commit: f074710 ("reject integratedTime w/o inclusionPromise (#1659)").
export function getTLogTimestamp(
entry: TransparencyLogEntry
-): TimestampVerificationResult {
+): TimestampVerificationResult | undefined {
+ // Only entries with an inclusion promise provide a verifiable timestamp
+ if (!entry.inclusionPromise) {
+ return undefined;
+ }
+
return {
type: 'transparency-log',
logID: entry.logId.keyId,
and in verifier.ts, verifyTimestamps now only pushes the result when it is
defined and compares timestamps.length against timestampThreshold (so an
inclusionProof-only entry no longer counts toward the threshold nor feeds
certificate validity).
Reproduction Steps
- Script:
bundle/repro/reproduction_steps.sh(self-contained; harness atbundle/repro/harness_test.ts). - What it does:
- Reuses/clones
sigstore/sigstore-jsinto the durable project cache (<project_cache_dir>/repo). - Resolves the fixed commit
f074710and its parent7845532(vulnerable,@sigstore/verify3.1.0). - For each commit:
git checkout,npm ci(if needed),npm run build(builds the workspace so jest can resolve@sigstore/bundle/core/jestvia theirdist), copies the harness intopackages/verify/src/__tests__/cve_repro.test.ts, and runs it vianpx jest --selectProjects verify --testPathPatterns cve_repro.test.ts. - The harness (run at both commits) exercises the real
@sigstore/verifysource and emits behavior markers to$REPRO_LOG:- Part A — callsite:
getTLogTimestamp()on an inclusionProof-only entry with an attacker-chosenintegratedTime. - Part B —
Verifier.verify()(public-key path) where the sole timestamp is the inclusionProof-only entry'sintegratedTime(timestampThreshold:1,tlogThreshold:0). - Part C —
Verifier.verify()(certificate path) on the real cert-signed fixtureV3.MESSAGE_SIGNATURE.TLOG_HASHEDREKORDV002, mutated so the inclusionProof-only entry'sintegratedTimeis a non-zero value inside the (expired) certificate's validity window and the RFC3161 timestamp is removed — making the unauthenticatedintegratedTimethe only timestamp source.
- Part A — callsite:
- Evaluates markers and writes
bundle/repro/runtime_manifest.jsonandbundle/repro/validation_verdict.json.
- Reuses/clones
- Expected evidence of reproduction:
bundle/logs/canonical.log(vulnerable) contains[CALLSITE_HIT]and[PROOF_MARKER], including the certificate-path line showing an expired cert accepted via unauthenticatedintegratedTime.bundle/logs/control.log(fixed) contains[NC_MARKER]only (getTLogTimestampreturnsundefined;Verifier.verify()throwsTIMESTAMP_ERROR), with no[PROOF_MARKER].- Script exits 0.
Evidence
- Marker logs:
bundle/logs/canonical.logbundle/logs/control.log
- Raw jest output:
bundle/logs/canonical_jest.logbundle/logs/control_jest.log
- Build logs:
bundle/logs/canonical_build.log,bundle/logs/control_build.log - Harness:
bundle/repro/harness_test.ts - Manifest/verdict:
bundle/repro/runtime_manifest.json,bundle/repro/validation_verdict.json
Key excerpts (vulnerable canonical run, bundle/logs/canonical.log):
[CALLSITE_HIT]
[PROOF_MARKER]: getTLogTimestamp() accepted UNAUTHENTICATED tlog integratedTime=1763174679 as a trusted timestamp (2025-11-15T02:44:39.000Z); entry has NO inclusionPromise (inclusionProof-only) -> integratedTime not cryptographically bound
[PROOF_MARKER]: Verifier.verify() (public-key path) SUCCEEDED; sole timestamp was UNAUTHENTICATED tlog integratedTime=1763174679 from inclusionProof-only entry -> timestampThreshold satisfied & key validity accepted on attacker-chosen time
[CALLSITE_HIT]
[PROOF_MARKER]: Verifier.verify() (certificate path) SUCCEEDED for a cert that is EXPIRED now (notBefore=2025-12-14T02:04:39.000Z, notAfter=2025-12-14T02:14:39.000Z, now=2026-07-02T18:09:39.516Z) using UNAUTHENTICATED tlog integratedTime=1765677909 from an inclusionProof-only entry -> certificate validity window satisfied by attacker-chosen, unauthenticated time
Key excerpts (fixed control run, bundle/logs/control.log):
[NC_MARKER]: getTLogTimestamp() returned undefined for inclusionProof-only entry (no inclusionPromise) -> untrusted integratedTime rejected
[NC_MARKER]: Verifier.verify() (public-key path) rejected with TIMESTAMP_ERROR -> inclusionProof-only integratedTime no longer counts toward timestampThreshold
[NC_MARKER]: Verifier.verify() (certificate path) rejected with TIMESTAMP_ERROR -> inclusionProof-only integratedTime not counted as a trusted timestamp; no signed timestamp present
- Environment: Node v24.18.0, npm 11.16.0, jest 30 with
@swc/jest(swc via bundled wasm binding). Repo built withtsc --build tsconfig.build.json. Vulnerable commit7845532(@sigstore/verify3.1.0); fixed commitf074710(@sigstore/verify3.1.1). Marker counts: canonicalCALLSITE_HIT=2 PROOF_MARKER=3; controlNC_MARKER=3 PROOF_MARKER=0.
Recommendations / Next Steps
- Upgrade: Consumers of
@sigstore/verifyshould upgrade to >= 3.1.1 (fix commitf074710). - Defense in depth: When accepting attacker-provided bundles, do not treat
integratedTimefrom inclusionProof-only entries as a trusted timestamp. Require a signedinclusionPromise/SET or an RFC3161 timestamp for any time-based decision (certificate validity,timestampThreshold). - Testing recommendations: Add regression tests that (a) feed an
inclusionProof-only entry with a non-zero
integratedTimeand assertgetTLogTimestampreturnsundefined, and (b) assert a bundle whose only timestamp is such an entry is rejected withTIMESTAMP_ERROR. The upstream fix already added thegetTLogTimestamp"no inclusion promise -> undefined" test; the certificate-validity impact path demonstrated here is a useful additional regression case.
Additional Notes
- Idempotency:
reproduction_steps.shwas run twice consecutively; both runs exited 0 with identical marker counts (canonical: CALLSITE_HIT=2, PROOF_MARKER=3;control: NC_MARKER=3, PROOF_MARKER=0). The script cleanspackages/verify/distand*.tsbuildinfoper commit and re-copies the harness, so runs are deterministic. - Surface alignment: The ticket's
claimed_surfaceislibrary_apiwithrequired_entrypoint_kind=function_call. The proof exercises the real@sigstore/verifylibrary functions (getTLogTimestamp,toSignedEntity,Verifier.verify,toTrustMaterial) through a jest harness, including a real cert-signed bundle fixture — matching the claimed surface. - Scope/limitations: The reproduction demonstrates the verification
authenticity/integrity bypass (an expired certificate accepted as valid via
an unauthenticated timestamp). It does not demonstrate code execution or
memory corruption (none is claimed). Part C uses a real sigstore bundle
fixture whose certificate is expired relative to the run time; the attacker
influence is the
integratedTimevalue, which is the exact unauthenticated field identified by the advisory.
Variant Analysis & Alternative Triggers for CVE-2026-48816
This is a bypass of the @sigstore/verify 3.1.1 fix (commit f074710,
"reject integratedTime w/o inclusionPromise (#1659)") for CVE-2026-48816. The
fix makes getTLogTimestamp() return undefined when entry.inclusionPromise
is absent, and stops counting such entries toward timestampThreshold. It
relies on verifyTLogs() → verifyTLogInclusion() → verifyTLogSET() to
cryptographically bind integratedTime whenever an inclusionPromise is
present. That binding only runs for entries in entity.tlogEntries; a
SignedEntity supplied to the public Verifier.verify(SignedEntity) API in
which the timestamp-providing tlog entry is not present in tlogEntries
(decoupled) carries a forged inclusionPromise + attacker-chosen
integratedTime. The fix's !entry.inclusionPromise presence-check passes
(because a forged inclusionPromise is present), the SET is never validated
(verifyTLogs iterates the empty tlogEntries), and verify() succeeds —
accepting an expired certificate at the attacker-chosen time. This reproduces
the original CVE's expired-cert-accepted impact on the fixed version. The
construction is identical in shape to the original CVE PoC's "Part B"
(tlogEntries: [], sole transparency-log timestamp). Through the standard
toSignedEntity(bundle) path the entries are coupled and a forged SET is
rejected by verifyTLogSET (negative control below), so the fix is effective for
the normal bundle path — but the fix's own logic does not enforce that coupling.
Fix Coverage / Assumptions
Invariant the fix relies on: "If entry.inclusionPromise is present, then
entry.integratedTime is cryptographically bound." The fix itself does not
verify the SET; it delegates authentication to verifyTLogSET() inside
verifyTLogs(). verifyTLogSET() (packages/verify/src/tlog/set.ts) canonicalizes
{body, integratedTime, logIndex, logID} and verifies the SET signature against a
trusted tlog public key — so a real SET does bind integratedTime, a tampered
integratedTime invalidates a real SET, and a forged SET fails verification.
Code path(s) the fix explicitly covers:
getTLogTimestamp()(packages/verify/src/timestamp/index.ts:44) — returnsundefinedwhen!entry.inclusionPromise(presence-check at :48).verifyTimestamps()(packages/verify/src/verifier.ts:87) — only pushes atransparency-logtimestamp whengetTLogTimestampreturns a defined result, and comparestimestamps.lengthagainsttimestampThreshold(:117).
What the fix does NOT cover:
- It checks
inclusionPromisepresence, not validity, ingetTLogTimestamp(). Verifier.verify()(packages/verify/src/verifier.ts:73) runsverifyTimestamps()+verifySigningKey()(certificate validity at the tlog timestamp) beforeverifyTLogs()(:74–76). Nothing requires that a timestamp-providing tlog entry also be inclusion-verified before its timestamp is trusted.verifyTLogs()(:165) iterates onlyentity.tlogEntries— neverentity.timestamps. So the SET of an entry that appears intimestampsbut not intlogEntriesis never validated.- The coupling between
timestampsandtlogEntriesis a property of thetoSignedEntity()helper (packages/verify/src/bundle/index.ts:28), not of theVerifiercontract.SignedEntityis an exported public type andVerifier.verify(SignedEntity)is the public method; nothing enforces that a caller usedtoSignedEntity().
Variant / Alternate Trigger
Entry point: the public Verifier.verify(entity: SignedEntity) API in
@sigstore/verify (packages/verify/src/verifier.ts:73), where entity is a
SignedEntity constructed with:
tlogEntries: [](empty — soverifyTLogs()is a no-op),timestamps: [{ $case: 'transparency-log', tlogEntry: E }]whereEhasintegratedTime = <attacker-chosen>andinclusionPromise = { signedEntryTimestamp: <forged bytes> },- a real cert-signed signature + an (now-expired) leaf certificate (for the cert-path demonstration), or a public-key hint (for the threshold demonstration).
Exact code path:
Verifier.verify()(:73) →verifyTimestamps(entity)(:74, :87).getTLogTimestamp(E)(packages/verify/src/timestamp/index.ts:44): the fix'sif (!entry.inclusionPromise) return undefined;(:48) passes becauseE.inclusionPromiseis the forged object → returns{ timestamp: new Date(Number(E.integratedTime)*1000) }(:55). The attacker-chosenintegratedTimeis now a "trusted" timestamp.verifyTimestampscounts it (timestamps.length≥timestampThreshold).verifySigningKey()(:75) →verifyCertificate(leaf, [attackerTime], …)(packages/verify/src/key/index.ts) checks the certificate chain + SCTs at the attacker-chosen time. The leaf cert is expired now but valid atattackerTime(chosen inside itsnotBefore..notAfter), so it is accepted.verifyTLogs(entity)(:76, :165) iteratesentity.tlogEntries(empty) → no-op →verifyTLogInclusion()/verifyTLogSET()(packages/verify/src/tlog/set.ts:33) never run → the forged SET is never validated.verifySignature()(:77) succeeds (real signature).verify()returns success on the FIXED version → expired cert accepted.
Why this is materially distinct from the original CVE trigger: the original
CVE used an inclusionProof-only entry (no inclusionPromise at all) and was
exploited through the coupled toSignedEntity(bundle) path. The fix gated
getTLogTimestamp on inclusionPromise presence to close that. The variant
defeats that gate by supplying a forged inclusionPromise whose SET is
never validated (decoupled SignedEntity), reaching the same
unauthenticated-integratedTime → trusted-timestamp → expired-cert sink. It is a
different entry shape, a different fix-assumption exploited (presence ≠
validity + decoupling), and the same sink.
Tested variants (all encoded in bundle/vuln_variant/variant_harness.ts):
Part 1 — BYPASS, cert path (headline): decoupled
SignedEntity+ forgedinclusionPromise+ attackerintegratedTime→verify()succeeds, expired cert accepted. Reproduced on both vulnerable and fixed versions.Part 0 — BYPASS, public-key path: same decoupling, public-key path →
timestampThresholdsatisfied on attacker-chosen unvalidated time. Reproduced on both versions.Part 2 — NEGATIVE CONTROL, bundle path (coupled): the same forged
inclusionPromisebut throughtoSignedEntity(bundle)(entry in bothtimestampsandtlogEntries) →verifyTLogSETrejects the forged SET withTLOG_INCLUSION_PROMISE_ERRORon both versions. Proves the fix's coupling protection holds for the standard bundle path and thatverifyTLogSETalways validated the SET (pre- and post-fix).Part 3 — ORIGINAL VECTOR baseline, bundle path (coupled): inclusionProof-only entry + attacker
integratedTime→[ORIG_OK]on vulnerable,[ORIG_REJECT](TIMESTAMP_ERROR) on fixed. Confirms the fix closes the original vector and sanity-checks the version under test.Package/component affected:
@sigstore/verify(npm), part of thesigstore/sigstore-jsmonorepo. Bypass confirmed on the fixed version3.1.1(commitf074710); also present on vulnerable3.1.0(7845532).Affected versions (as tested):
3.1.0(7845532) and3.1.1(f074710a91ea9260a9ac2142345634579843a3cd).Risk level / consequences: Medium (same CWE-345 class as the parent advisory, CVSS 6.5). Integrity impact: a consumer that passes an attacker-controlled
SignedEntitytoVerifier.verify()and relies on tlog-derived timestamps for certificate-validity /timestampThresholdcan be tricked into accepting a bundle whose signing certificate is expired (or otherwise time-invalid) via an unauthenticatedintegratedTimecarried by a forged, never-validatedinclusionPromise. Realism caveat: through the standardtoSignedEntity(bundle)path (the documented bundle attack surface) the fix is effective; the bypass requires aSignedEntityin which the timestamp's tlog entry is not intlogEntries, which the standard helper does not produce but the publicVerifier.verify(SignedEntity)API permits.
Impact Parity
- Disclosed/claimed maximum impact (parent): Integrity bypass — an
attacker-supplied bundle can influence time-based verification (certificate
validity windows and
timestampThreshold) via an unauthenticatedintegratedTime. - Reproduced impact from this variant run:
Verifier.verify()succeeds on the FIXED version for a real cert-signed bundle whose leaf certificate is expired now (notBefore=2025-12-14T02:04:39Z,notAfter=2025-12-14T02:14:39Z,now=2026-07-02…) because the forgedinclusionPromise's unauthenticatedintegratedTime(1765677909=notBefore+30s, inside the cert validity window) is trusted as the signing time. The public-key path likewise satisfiestimestampThreshold:1on the attacker-chosen, unvalidated time. - Parity:
full— the same expired-cert-accepted integrity impact as the parent, reproduced on the patched code path. - Not demonstrated: No code-execution / memory-safety primitive (this is a verification-logic integrity bypass, not a memory bug); no end-to-end network service exploitation (the entry point is the library API).
Root Cause
The same underlying bug — an unauthenticated integratedTime treated as a
trusted timestamp — can still be reached because the fix authenticates
integratedTime only indirectly, via verifyTLogSET() inside verifyTLogs(),
and verifyTLogs() iterates entity.tlogEntries rather than
entity.timestamps. getTLogTimestamp() itself performs only a presence-check
on inclusionPromise. Therefore any SignedEntity where a timestamp-providing
tlog entry is not in tlogEntries evades SET validation entirely; a forged
inclusionPromise satisfies the presence-check and the attacker-chosen
integratedTime is trusted — driving verifySigningKey/verifyCertificate to
accept an expired certificate. The fix is correct for the
toSignedEntity(bundle) coupling but incomplete as a property of the
Verifier.verify(SignedEntity) contract.
Fix commit: f074710a91ea9260a9ac2142345634579843a3cd
(https://github.com/sigstore/sigstore-js commit f074710,
"reject integratedTime w/o inclusionPromise (#1659)").
Reproduction Steps
- Script:
bundle/vuln_variant/reproduction_steps.sh(self-contained; harness atbundle/vuln_variant/variant_harness.ts). - What it does:
- Reuses/clones
sigstore/sigstore-jsinto the durable project cache (<project_cache_dir>/repo). - Resolves the fixed commit
f074710and its parent7845532(vulnerable,@sigstore/verify3.1.0). Recordsgit rev-parsefor both (source_identity.json). - For each commit:
git checkout, clean built artifacts,npm ci(if needed),npm run build, copy the variant harness intopackages/verify/src/__tests__/cve_variant.test.ts, and run it vianpx jest --selectProjects verify --testPathPatterns cve_variant.test.ts. - Restores the cache repo checkout to its original state (
f074710) on exit (trap) and removes the harness copy, so the repo is left clean. - Evaluates markers and writes
runtime_manifest.json,validation_verdict.json,source_identity.json,root_cause_equivalence.json. Exits 0 if the bypass reproduced on the fixed version, else 1.
- Reuses/clones
- Expected evidence of reproduction:
bundle/logs/vuln_variant_fixed.logcontains[BYPASS_OK](Part 1 cert path: expired cert accepted) and[BYPASS_OK](Part 0 public-key path), plus[NC_REJECT](Part 2 coupled bundle path rejects forged SET) and[ORIG_REJECT](Part 3 original vector closed).bundle/logs/vuln_variant_vuln.logcontains[BYPASS_OK]x2,[NC_REJECT], and[ORIG_OK](original CVE open).- Script exits 0.
Evidence
- Marker logs:
bundle/logs/vuln_variant_fixed.logbundle/logs/vuln_variant_vuln.log
- Raw jest output:
bundle/logs/vuln_variant_fixed_jest.log(4 tests passed)bundle/logs/vuln_variant_vuln_jest.log(4 tests passed)
- Build logs:
bundle/logs/vuln_fixed_build.log,bundle/logs/vuln_vuln_build.log(and*_npmci.logif installed). - Harness:
bundle/vuln_variant/variant_harness.ts - Manifests/verdict:
bundle/vuln_variant/runtime_manifest.json,bundle/vuln_variant/validation_verdict.json,bundle/vuln_variant/source_identity.json,bundle/vuln_variant/root_cause_equivalence.json,bundle/vuln_variant/variant_manifest.json.
Key excerpts — fixed run (bundle/logs/vuln_variant_fixed.log):
[CALLSITE_BYPASS]: getTLogTimestamp() returned a trusted timestamp (2025-12-14T02:05:09.000Z) for an entry whose inclusionPromise is FORGED (presence-check passes, SET not yet validated); integratedTime=1765677909
[BYPASS_OK]: Verifier.verify() (cert path) SUCCEEDED on a DECOUPLED SignedEntity (tlogEntries empty) whose sole timestamp is a tlog entry carrying a FORGED inclusionPromise + attacker integratedTime=1765677909; cert is EXPIRED now (notBefore=2025-12-14T02:04:39.000Z, notAfter=2025-12-14T02:14:39.000Z, now=2026-07-02T18:16:12.815Z); fix presence-check passed, SET never validated -> original CVE impact (expired-cert-accepted) reproduced on this version
[BYPASS_OK]: Verifier.verify() (public-key path) SUCCEEDED on a DECOUPLED SignedEntity whose sole timestamp is a tlog entry with a FORGED inclusionPromise + attacker integratedTime=1763174679; timestampThreshold satisfied & key validity accepted on attacker-chosen, unvalidated time
[NC_REJECT]: bundle-path (coupled) verify rejected a FORGED inclusionPromise with code=TLOG_INCLUSION_PROMISE_ERROR: inclusion promise could not be verified -> verifyTLogSET validated the SET and rejected (coupling protection holds on this version)
[ORIG_REJECT]: original vector (inclusionProof-only) verify rejected with code=TIMESTAMP_ERROR: expected 1 timestamps, got 0 -> fix closes the original inclusionProof-only vector on this version
Marker counts: fixed BYPASS_OK=2 NC_REJECT=1 ORIG_REJECT=1; vuln
BYPASS_OK=2 NC_REJECT=1 ORIG_OK=1.
Environment: Node v24.18.0, npm 11.16.0, jest 30 with @swc/jest. Repo
built with tsc --build (npm run build). Fixed commit
f074710a91ea9260a9ac2142345634579843a3cd (@sigstore/verify 3.1.1); vulnerable
commit 7845532f9d17f6f765363dbee82b01bd159fb52b (@sigstore/verify 3.1.0).
Both resolved via git rev-parse from the cloned repo in the durable project
cache (bundle/vuln_variant/source_identity.json).
Recommendations / Next Steps
The fix should not rely on the toSignedEntity() coupling to authenticate
integratedTime. One or more of:
- Authenticate at the timestamp source. Verify the SET inside/alongside
getTLogTimestamp()(or inverifyTimestamps()) against the trusted tlog material before trustingintegratedTime— make the timestamp decision independent ofverifyTLogs(). This directly closes the decoupled bypass. - Enforce coupling in the verifier. Require that every
transparency-logtimestamp'stlogEntryis also a member ofentity.tlogEntriesand has had its inclusion verified, before its timestamp is used for certificate-validity / threshold decisions. - Do not gate on presence alone. Treat
inclusionPromiseas a timestamp source only after its SET has been verified, not merely because the field is non-null.
Note: simply reordering verifyTLogs() before verifyTimestamps() is
not sufficient — with tlogEntries: [] and tlogThreshold: 0,
verifyTLogs() is still a no-op, so the decoupled bypass would remain. Option 1
or 2 is required.
Testing recommendations: add a regression test that feeds a decoupled
SignedEntity (timestamp tlog entry absent from tlogEntries) with a forged
inclusionPromise + attacker integratedTime and asserts verify() rejects
(TIMESTAMP_ERROR / TLOG error) rather than accepting an expired certificate.
Additional Notes
- Idempotency:
reproduction_steps.shwas run three times consecutively; all runs exited 0 with identical markers. The cache repo checkout is restored tof074710on every exit (trap), and the harness copy is removed, so the repo is left in the same state the repro stage left it (no untracked files). - Threat-model scope:
sigstore-jshas no in-repoSECURITY.md;README.mdreferences sigstore's security process. The CVE class is CWE-345 (Insufficient Verification of Data Authenticity), so trusting an unauthenticated value in the verifier is explicitly in-scope. This bypass is the same class and sink, not a documented limitation. - Realism caveat (explicit): The bypass requires a
SignedEntitynot produced by the standardtoSignedEntity(bundle)coupling. Through the normal bundle attack surface (the advisory's "attacker supplying a malicious bundle"), the fix is effective (Part 2 negative control). The bypass is reachable via the publicVerifier.verify(SignedEntity)API and matches the construction the original CVE PoC Part B used.same_surface_confidenceis set tomediumandexploitability_confidencetomediumto reflect this caveat;same_root_cause_confidenceishigh(identical sink and impact). - Negative control confirms fix coverage: Part 2 shows the coupled bundle
path rejects a forged
inclusionPromiseon both versions (verifyTLogSETalways validated the SET), so the fix does not regress the bundle path and the bypass is specifically about the decoupled construction.
CVE-2026-48816 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-48816
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-48816
Upgrade sigstore/sigstore-js · github to 3.1.1 or later.
FAQ: CVE-2026-48816
How does the sigstore-js timestamp-forgery attack work?
How severe is CVE-2026-48816?
Where is CVE-2026-48816 fixed?
How can I reproduce CVE-2026-48816?
References for CVE-2026-48816
Authoritative sources for CVE-2026-48816 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.