Skip to content

CVE-2026-48816: Verified Reproduction

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.

REPRO-2026-00210 sigstore/sigstore-js · github Jul 2, 2026 CVE entry .txt
Severity
MEDIUM
CVSS
6.5
Confidence
HIGH
Reproduced in
18m 54s
Tool calls
158
Spend
$2.52
01 · Overview

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).

02 · Severity & CVSS

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 threat level
6.5 / 10 CVSS base
Weakness CWE-345 (Insufficient Verification of Data Authenticity)

Medium — meaningful risk under specific conditions. Schedule a fix in the normal cycle.

How to Reproduce CVE-2026-48816

$ pruva-verify REPRO-2026-00210
or curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00210/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-48816

Security impact — reproduced
  • reached the target end-to-end
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

tlogEntries[].integratedTime in an attacker-supplied sigstore bundle (inclusionProof-only entry, no signed inclusionPromise)

Attack chain
  1. Verifier.verify
  2. verifyTimestamps
  3. 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
Runnable proof: reproduction_steps.sh
Captured evidence: fixed buildvuln variant fixed jest
How the agent worked 368 events · 158 tool calls · 19 min
19 minDuration
158Tool calls
104Reasoning steps
368Events
1Dead-ends
Agent activity over 19 min
Support
30
Hypothesis
2
Repro
159
Judge
43
Variant
130
0:0018:54

Root Cause and Exploit Chain for CVE-2026-48816

Versions: @sigstore/verify 3.1.0 (vulnerable). Fixed in 3.1.1.Fixed: commit: f074710 ("reject integratedTime w/o inclusionPromise

@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 the sigstore/sigstore-js monorepo. Affected source files:
    • packages/verify/src/bundle/index.tstoSignedEntity adds a transparency-log timestamp for every tlog entry where integratedTime != '0', regardless of whether an inclusionPromise is present.
    • packages/verify/src/timestamp/index.tsgetTLogTimestamp converts entry.integratedTime into a Date with no check that the value is cryptographically bound.
    • packages/verify/src/verifier.tsverifyTimestamps counts every transparency-log timestamp toward timestampThreshold, and verify() runs timestamp (and therefore certificate-validity) checks before verifyTLogs (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 the inclusionPromise/SET path (verifyTLogSET) signs over integratedTime; the inclusionProof path does not.
  • Affected versions: @sigstore/verify 3.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/verify 3.1.0).
    • Fixed commit: f074710 ("reject integratedTime w/o inclusionPromise (#1659)"), released as 3.1.1 via c1dc7d4 ("Version Packages (#1607)").
  • 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 unauthenticated integratedTime in an inclusionProof-only tlog entry.
  • Reproduced impact from this run:
    • The real getTLogTimestamp() callsite accepts an inclusionProof-only entry's integratedTime as a trusted timestamp (vulnerable) and returns undefined for it (fixed).
    • The real Verifier.verify() succeeds on a real cert-signed bundle mutated to be inclusionProof-only with an attacker-chosen integratedTime, accepting a certificate that is expired now (notAfter = 2025-12-14T02:14:39Z, now = 2026-07-02…) because the unauthenticated integratedTime is set inside the cert's validity window (notBefore = 2025-12-14T02:04:39Z). The fixed build rejects the identical bundle with TIMESTAMP_ERROR.
  • Parity: full — the disclosed trust gap (unauthenticated integratedTime used for certificate validity / timestampThreshold) is demonstrated through the real @sigstore/verify verification 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

  1. Script: bundle/repro/reproduction_steps.sh (self-contained; harness at bundle/repro/harness_test.ts).
  2. What it does:
    • Reuses/clones sigstore/sigstore-js into the durable project cache (<project_cache_dir>/repo).
    • Resolves the fixed commit f074710 and its parent 7845532 (vulnerable, @sigstore/verify 3.1.0).
    • For each commit: git checkout, npm ci (if needed), npm run build (builds the workspace so jest can resolve @sigstore/bundle/core/jest via their dist), copies the harness into packages/verify/src/__tests__/cve_repro.test.ts, and runs it via npx jest --selectProjects verify --testPathPatterns cve_repro.test.ts.
    • The harness (run at both commits) exercises the real @sigstore/verify source and emits behavior markers to $REPRO_LOG:
      • Part A — callsite: getTLogTimestamp() on an inclusionProof-only entry with an attacker-chosen integratedTime.
      • Part BVerifier.verify() (public-key path) where the sole timestamp is the inclusionProof-only entry's integratedTime (timestampThreshold:1, tlogThreshold:0).
      • Part CVerifier.verify() (certificate path) on the real cert-signed fixture V3.MESSAGE_SIGNATURE.TLOG_HASHEDREKORDV002, mutated so the inclusionProof-only entry's integratedTime is a non-zero value inside the (expired) certificate's validity window and the RFC3161 timestamp is removed — making the unauthenticated integratedTime the only timestamp source.
    • Evaluates markers and writes bundle/repro/runtime_manifest.json and bundle/repro/validation_verdict.json.
  3. 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 unauthenticated integratedTime.
    • bundle/logs/control.log (fixed) contains [NC_MARKER] only (getTLogTimestamp returns undefined; Verifier.verify() throws TIMESTAMP_ERROR), with no [PROOF_MARKER].
    • Script exits 0.

Evidence

  • Marker logs:
    • bundle/logs/canonical.log
    • bundle/logs/control.log
  • Raw jest output:
    • bundle/logs/canonical_jest.log
    • bundle/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 with tsc --build tsconfig.build.json. Vulnerable commit 7845532 (@sigstore/verify 3.1.0); fixed commit f074710 (@sigstore/verify 3.1.1). Marker counts: canonical CALLSITE_HIT=2 PROOF_MARKER=3; control NC_MARKER=3 PROOF_MARKER=0.

Recommendations / Next Steps

  • Upgrade: Consumers of @sigstore/verify should upgrade to >= 3.1.1 (fix commit f074710).
  • Defense in depth: When accepting attacker-provided bundles, do not treat integratedTime from inclusionProof-only entries as a trusted timestamp. Require a signed inclusionPromise/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 integratedTime and assert getTLogTimestamp returns undefined, and (b) assert a bundle whose only timestamp is such an entry is rejected with TIMESTAMP_ERROR. The upstream fix already added the getTLogTimestamp "no inclusion promise -> undefined" test; the certificate-validity impact path demonstrated here is a useful additional regression case.

Additional Notes

  • Idempotency: reproduction_steps.sh was 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 cleans packages/verify/dist and *.tsbuildinfo per commit and re-copies the harness, so runs are deterministic.
  • Surface alignment: The ticket's claimed_surface is library_api with required_entrypoint_kind=function_call. The proof exercises the real @sigstore/verify library 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 integratedTime value, which is the exact unauthenticated field identified by the advisory.

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.

Event 1/40
0:001:49
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · CVE-2026-48816 · REPRO-20
0:03
0:05
web search
0:06
web search
0:09
0:11
web search
0:12
0:14
0:15
0:18
0:19
web search
0:22
0:24
web search
0:26
1:10
1:12
web search
1:16
1:18
1:29
1:29
extract_facts
no facts extracted
1:31
1:31
1:31
supportrepro
1:45
1:47
1:47
1:47
1:49
08 · How to Fix

How to Fix CVE-2026-48816

Upgrade sigstore/sigstore-js · github to 3.1.1 or later.

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-48816 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-48816

How does the sigstore-js timestamp-forgery attack work?

Because only the signed inclusionPromise/SET path (verifyTLogSET) actually signs over integratedTime, an attacker who supplies a bundle with an inclusionProof-only tlog entry can choose integratedTime freely - for example making an expired Fulcio certificate appear to have been valid at signing time - and the verifier's toSignedEntity/getTLogTimestamp/verifyTimestamps logic will accept it.

How severe is CVE-2026-48816?

It is rated medium severity - it lets an attacker manipulate a time-based verification decision, such as certificate expiry, rather than forge a signature outright.

Where is CVE-2026-48816 fixed?

It is fixed in @sigstore/verify 3.1.1 - upgrade to 3.1.1 or later.

How can I reproduce CVE-2026-48816?

Download the verified script from this page and run it in an isolated environment; it constructs a bundle with an inclusionProof-only tlog entry and an attacker-chosen integratedTime, then shows the vulnerable @sigstore/verify accepting a certificate outside its real validity window while the patched 3.1.1 rejects it.
11 · References

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.