This is a weak serial number on a sealed envelope, and it only matters if someone can already read the mail
CVE-2025-21617 is a low-entropy nonce bug in guzzlehttp/oauth-subscriber, the PHP middleware that signs Guzzle requests with OAuth 1.0. Affected versions are all releases before 0.8.1—practically the 0.1.0 through 0.8.0 line. Before the fix, the library generated oauth_nonce values predictably enough that a replay attack becomes more realistic if signed requests traverse the network without TLS protection.
In real enterprise conditions this is not a front-of-queue patch. The decisive friction is the attack precondition: the attacker typically needs on-path visibility or another way to capture OAuth 1.0 traffic *and* the deployment has to permit non-TLS transport or equivalent plaintext exposure on an internal segment. That sharply narrows the reachable population, so despite the security smell and the wide package install base, this lands as = ASSESSED AT LOW.
4 steps from start to impact.
Find a live OAuth 1.0 client on a vulnerable package
guzzlehttp/oauth-subscriber prior to 0.8.1 and confirms it still signs requests with OAuth 1.0. Typical tooling here is basic package enumeration from leaked composer.lock, SBOMs, source disclosure, or CI artifact access rather than an internet-facing exploit scanner.- Target uses
guzzlehttp/oauth-subscriber<0.8.1 - Application still relies on OAuth 1.0 signing
- Attacker can identify the dependency or observe traffic shape
- Many modern PHP estates no longer use OAuth 1.0 at all
- This is a library flaw, so there is no clean Shodan/Censys fingerprint
- SCA usually catches the package once inventories exist
Get on-path access to signed traffic
mitmproxy, tcpdump, or a compromised reverse proxy. The advisory explicitly ties impact to cases where TLS is not used, which means plaintext transport or equivalent internal visibility is the meaningful setup.- Attacker has network position to observe client-to-server requests
- TLS is absent, broken, bypassed internally, or traffic is otherwise exposed in plaintext
- HTTPS, mTLS, and API gateways kill this step in most sane deployments
- If TLS is end-to-end, the nonce weakness is mostly academic
- Requiring on-path visibility usually implies the attacker is already inside
Replay or predict a usable OAuth request
curl, Burp Repeater, or a custom script. The vulnerable code used a predictable nonce construction, later replaced with random_bytes(20), so the practical abuse case is making duplicate or guessable OAuth requests acceptable inside the server's nonce/timestamp handling window.- Server accepts OAuth 1.0 requests from this client
- Server-side replay protections are weak enough for the predictable nonce to matter
- Attacker can resend within the accepted timestamp/nonce window
- Many OAuth 1.0 providers reject duplicate nonces aggressively
- Timestamp windows are short
- If the attacker can already replay raw plaintext traffic, the nonce bug only slightly improves their odds
oauth_nonce, repeated oauth_timestamp, or duplicate business transactions. Most vuln scanners will not validate exploitability.Gain limited business impact
- Captured request performs a sensitive or non-idempotent action
- Downstream application lacks its own anti-replay or duplicate-transaction logic
- Many APIs add app-layer idempotency controls or transaction deduplication
- Read-only calls reduce impact to limited confidentiality exposure
- Blast radius is usually one client integration, not whole-host compromise
The supporting signals.
| In-the-wild status | No KEV listing was provided in the prompt, and I found no credible public reporting of active exploitation for this CVE. |
|---|---|
| Proof-of-concept availability | No standalone weaponized PoC stood out in public search results. The public fix commit is enough for a competent attacker to understand the bug. |
| EPSS | Prompt-supplied EPSS is 0.00409 (0.409%), which is low signal for near-term exploitation pressure. |
| KEV status | Not KEV-listed per the prompt; no urgency boost from CISA exploitation evidence. |
| CVSS context | No vendor baseline was supplied for comparison, so noisgate treats this as a first-principles assessment. The public CVSS context visible in NVD/OSV is CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N, which already hints at high attack complexity and limited direct impact. |
| Affected versions | Affected package is guzzlehttp/oauth-subscriber before 0.8.1; OSV enumerates the vulnerable line as 0.1.0 through 0.8.0. |
| Fixed versions | Upgrade to 0.8.1 or later. I found no distro backport guidance tied to this package; treat this as a normal Composer dependency update. |
| Exposure reality | This is a library, not a remotely fingerprintable appliance. Shodan/Censys/GreyNoise-style internet counts are not meaningful here; runtime exposure depends on whether an app still uses OAuth 1.0 and sends those requests without end-to-end TLS. |
| Ecosystem reach | Packagist shows roughly 14.9M installs and 164 dependents, so the package is common enough that inventory hits will exist, even if exploitability is narrow. |
| Disclosure and credit | Published 2025-01-06 in GitHub advisory GHSA-237r-r8m4-4q88; credited finder is psyker156. |
noisgate verdict.
The single biggest suppressor is the attacker-position requirement: this bug matters mainly when OAuth 1.0 traffic is exposed without TLS, which usually means an on-path or already-inside adversary. That turns a theoretically network-reachable flaw into a niche, post-compromise-style abuse case with limited blast radius.
Why this verdict
- Requires plaintext or equivalent visibility: the advisory itself says replay risk materializes when TLS is not used, so secure transport removes most real-world exposure.
- Attacker position is unfavorable: practical abuse usually needs on-path access, implying internal foothold, proxy compromise, or another prior-stage win before this CVE matters.
- Blast radius is narrow: impact is limited to replaying the specific OAuth-signed action available to that client integration, not host takeover or broad tenant escape.
Why not higher?
This is not an unauthenticated internet-to-RCE story. To matter operationally, the deployment usually has to be using legacy OAuth 1.0, running a vulnerable dependency, and exposing signed traffic without proper TLS or equivalent confidentiality. Those are compounding friction points, not edge-case details.
Why not lower?
It is still a real security defect in a broadly used package, and some enterprises absolutely have internal HTTP hops, legacy API gateways, or test/prod exceptions that break the 'TLS everywhere' assumption. If the signed request performs a non-idempotent action, replay can become a legitimate business-risk issue rather than a paper cut.
What to do — in priority order.
- Enforce TLS end to end — Make sure OAuth 1.0 requests are protected from client through every proxy hop to the API origin, not just at the internet edge. For a LOW verdict there is no formal noisgate mitigation SLA; treat this as backlog hygiene, but fix any plaintext exception immediately if you discover one because that is the real enabler.
- Block cleartext egress for OAuth clients — Use proxy policy, service mesh policy, or firewall rules to prevent app tiers from sending OAuth 1.0 requests over plain HTTP. Again, LOW means no formal mitigation SLA, so fold this into routine network-hardening work unless you find an active non-TLS path.
- Add replay rejection server-side — Where you own the receiving API, verify nonce uniqueness and timestamp windows tightly, and add idempotency controls for state-changing operations. Treat this as backlog hardening for low-severity exposure, especially on legacy integrations you cannot retire quickly.
- Inventory and retire OAuth 1.0 usage — The best long-term control is reducing the population that still depends on OAuth 1.0 signing at all. Because the verdict is LOW, schedule this with normal architecture debt reduction rather than emergency response.
- A WAF does not solve predictable client nonce generation; the flaw is in how the request is signed before it ever hits the application perimeter.
- EDR on the client host will not reliably catch protocol-level replay if the attacker is just resending captured HTTP requests from elsewhere.
- Rotating OAuth secrets alone does not fix the weak nonce behavior; it changes credentials, not the replay surface created by predictable or observable requests.
Crowdsourced verification payload.
Run this on the application host, CI workspace, or any auditor workstation that can read the target project's composer.lock. Invoke it as python3 check_cve_2025_21617.py /path/to/app or point directly at the lockfile like python3 check_cve_2025_21617.py /var/www/html/composer.lock. It needs only read access; no admin privileges are required.
#!/usr/bin/env python3
# Check for CVE-2025-21617 in guzzlehttp/oauth-subscriber
# Usage: python3 check_cve_2025_21617.py /path/to/app-or-composer.lock
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
import json
import os
import re
import sys
PACKAGE = 'guzzlehttp/oauth-subscriber'
FIXED = (0, 8, 1)
def normalize_version(v):
if v is None:
return None
v = v.strip()
if v.startswith('v'):
v = v[1:]
v = v.split('@', 1)[0]
if any(tag in v.lower() for tag in ['dev', 'alpha', 'beta', 'rc', 'patch', 'pl']):
# Composer branches / pre-release naming are ambiguous for this simple offline check
return None
m = re.match(r'^(\d+)\.(\d+)\.(\d+)(?:[.+-].*)?$', v)
if not m:
return None
return tuple(int(x) for x in m.groups())
def compare_version(a, b):
return (a > b) - (a < b)
def load_lockfile(path):
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def find_package(data, name):
for section in ('packages', 'packages-dev'):
for pkg in data.get(section, []) or []:
if pkg.get('name') == name:
return pkg
return None
def resolve_target(arg):
if os.path.isdir(arg):
candidate = os.path.join(arg, 'composer.lock')
if os.path.isfile(candidate):
return candidate
return None
if os.path.isfile(arg) and os.path.basename(arg) == 'composer.lock':
return arg
return None
def main():
if len(sys.argv) != 2:
print('UNKNOWN - usage: python3 check_cve_2025_21617.py /path/to/app-or-composer.lock')
sys.exit(2)
target = resolve_target(sys.argv[1])
if not target:
print('UNKNOWN - composer.lock not found')
sys.exit(2)
try:
data = load_lockfile(target)
except Exception as e:
print(f'UNKNOWN - failed to read lockfile: {e}')
sys.exit(2)
pkg = find_package(data, PACKAGE)
if not pkg:
print(f'UNKNOWN - package {PACKAGE} not present in lockfile')
sys.exit(2)
raw_version = pkg.get('version')
parsed = normalize_version(raw_version)
if parsed is None:
print(f'UNKNOWN - package present but version is non-semver or ambiguous: {raw_version}')
sys.exit(2)
if compare_version(parsed, FIXED) < 0:
print(f'VULNERABLE - {PACKAGE} {raw_version} < 0.8.1')
sys.exit(1)
print(f'PATCHED - {PACKAGE} {raw_version} >= 0.8.1')
sys.exit(0)
if __name__ == '__main__':
main()
If you remember one thing.
guzzlehttp/oauth-subscriber to 0.8.1+ in the next normal dependency-maintenance cycle while documenting the rationale for anything left temporarily unpatched.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.