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.
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://www.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 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.
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.