DEV Community

Davi
Davi

Posted on Originally published at blog.mago.team

OAuth Access Token Leakage via Logs and APM: The RFC 6750 Warning Nobody Enforced

CVE-2024-47822 hit Directus deployments running with LOG_STYLE=raw enabled. The vulnerability was not code injection or an authentication bypass. The access token was already in the URL; the logging system wrote what it received, exactly as designed.

Every API that accepts ?access_token= in the URL leaks credentials to every system that logs HTTP requests. Web servers, CDNs, APM agents, and error trackers capture those tokens by default, without any operator configuration. Retention persists indefinitely until log rotation.

RFC 6750 Prohibited This in 2012 and APIs Still Do It

RFC 6750 §5.3 does not suggest avoiding tokens in URLs. It uses "SHOULD NOT" with an explicitly documented threat model: browsers, web servers, and proxies log the full URL by default.

The specification text: "Bearer tokens SHOULD NOT be passed in page URLs. Browsers, web servers, and other software may not adequately secure URLs in the browser history, web server logs, and other data structures." Section 2.3 permits the query string method only when other transport is impossible, listing the security deficiencies as justification for the warning.

The industry treated "SHOULD NOT" as optional. APIs kept accepting ?access_token= for browser compatibility, link sharing, and webhook callbacks. CVE-2024-47822 proved in 2024 exactly what the RFC described as a threat in 2012.

CVE-2024-47822: Directus Logs the Raw Query String

With LOG_STYLE=raw enabled, Directus logged the full req.query object, including access_token. The CVSS score of 4.2 rates the flaw as moderate. The real impact depends entirely on where those logs were stored and how long they remained accessible.

Directus static tokens, used for integrations, have no expiration and no refresh mechanism. A log exposure requires manual rotation of every compromised token, with no guarantee the exposure window can be determined precisely. The researcher who reported the flaw found the tokens in their own ELK stack after enabling raw log mode for performance diagnostics.

The patch landed in versions 10.13.2 and 11.1.0 in October 2024. The fix stripped the token from the query string before any log was written. Directus needed a CVE in 2024 to do what the RFC recommended in 2012.

The Callback URL Problem: nhost and Browser History

GHSA-g2qj-prgh-4g9r documented the nhost auth service appending refreshToken as a query parameter to the OAuth callback URL. The code was direct: values.Add("refreshToken", session.RefreshToken), producing URLs in the format /callback?refreshToken=eyJ....

Refresh tokens outlive access tokens. A compromised refresh token grants the ability to obtain new access tokens indefinitely. The token appeared across 3 simultaneous persistence surfaces: browser history, the web server access.log, and CDN edge logs.

Cloudflare logs ClientRequestURI with the full query string by default in Enterprise accounts via Logpush. Those logs are routinely exported to S3 buckets or third-party log management systems. The patch landed in auth service version 0.48.0; earlier installations with OAuth enabled exposed refresh tokens in access logs throughout their entire period of use.

Log Maskers Fail on Token Format Edge Cases

GHSA-f9f8-rm49-7jv2 affected Composer after GitHub changed the token format in 2022 to include hyphens: ghs_<id>_<base64url>. Composer's validation regex did not accept hyphens and rejected the token, including the full GITHUB_TOKEN value in the exception message.

The GitHub Actions secret masker does exact substring matching against the configured secret value. Build tools injected ANSI color codes into the output and fragmented the token string mid-match. The masker found no pattern match and the full GITHUB_TOKEN reached the build logs in plaintext.

The pattern is general: any masker doing exact string search fails when the log output wraps, URL-encodes, or applies different formatting to the stream. APM agents face the same problem at scale, with no ability to enumerate all JWT-shaped strings in advance.

The Full Observability Stack as a Capture Surface

A single HTTP request carrying ?access_token= touches at least 5 independent systems. Each writes the full URL to durable storage without any misconfiguration required.

Nginx with the combined format includes $request in the log by default, containing the full URI with query string. Cloudflare stores ClientRequestURI in Logpush datasets with the query string included by default.

Datadog APM assigns the full URL value to the http.url tag by default. The DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP variable uses heuristics and can fail silently on ?access_token=<JWT> when the parameter name is not in the configured denylist.

New Relic disables query string capture by default, but capture_request_params enables it. Teams turning on full request tracing for diagnostics may not realize tokens are being captured in traces.

Sentry adds the query string to the HTTP request context and exception breadcrumbs. The default denylist covers password, session, and csrf, but access_token may not match the pattern depending on the SDK version. The beforeSend hook is required for any guarantee that tokens stay local.

The MAGO Intel tool (intel.mago.team) scans API endpoints for query-parameter token exposure. It checks whether ?access_token=, ?token=, and ?api_key= are accepted and whether the server reflects those values in response headers or error messages.

Detection: Grep the Logs You Already Have

JWT-shaped strings in access logs are detectable in under a minute with a single grep. The prefix eyJ is the base64 encoding of {", present in the header of every valid JWT.

# nginx or Apache access.log
grep -E 'access_token=eyJ|\?token=eyJ|\?api_key=eyJ' /var/log/nginx/access.log | wc -l
Enter fullscreen mode Exit fullscreen mode

In the Datadog APM trace explorer, filter by @http.url:*access_token=* to surface spans where tokens appeared in URLs. In Sentry, the query event.type:transaction request.query_string:*access_token* returns exceptions that captured tokens in the URL context.

Most teams discover the leakage only after enabling their full observability stack for diagnostics. Check CDN log exports too if you use Cloudflare Enterprise or Fastly with Logpush active.

Fix: Use the Authorization Header, Gate the Query String

The standard is Authorization: Bearer <token>. No APM agent or web server logs that header by default. Nginx excludes the Authorization header from the access log in the combined format.

For legacy clients that cannot send headers: use short-lived signed URLs, following the S3 pre-signed URL pattern. The token never appears in plaintext in the URL, and the signature carries a forced expiration. For webhook callbacks: move the token to the POST body instead of the query string.

# Replace $request with $uri to strip query string from the access log
log_format main '$remote_addr - $remote_user [$time_local] "$uri" '
                '$status $body_bytes_sent';
Enter fullscreen mode Exit fullscreen mode
# Datadog: disable query string capture entirely
DD_HTTP_SERVER_TAG_QUERY_STRING=false

# Datadog: redact JWT-shaped values specifically
DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP='eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'
Enter fullscreen mode Exit fullscreen mode

In Sentry, add a beforeSend hook to strip event.request.query_string before any event leaves the local environment. Without it, tokens in query strings reach Sentry's servers before any downstream scrubbing.

To audit the current state: grep -r 'access_token' nginx.conf plus a review of CDN logging settings and Datadog obfuscation configuration. Rotate all static tokens that appeared in URL parameters before applying any fix. The log files already exist and may have been shipped to third parties throughout the entire retention period.

The IETF wrote RFC 6750 §5.3 in 2012. Every system that logs HTTP requests has been collecting those tokens ever since. The question is not whether your logs contain access tokens. It is whether you have looked.

Top comments (0)