Skip to content

HTTP redirects bypass the URL allowlist in lookup table data adapters and event notifications, enabling non-admin SSRF and internal data exfiltration #26591

Description

@geo-chen

commit abb14d1 (7.2.0-SNAPSHOT); reproduced on graylog/graylog:7.1.4

Summary

Graylog's URL allowlist (UrlAllowlistService) is the sole SSRF mitigation for every user-configurable outbound-URL feature: HTTP event notifications, and HTTP JSON path / DSV lookup table data adapters. The allowlist check is performed once, against the URL string configured by the user, before the request is issued. None of the HTTP clients used by these features disable following HTTP redirects. As a result, if the allowlisted URL (an external, admin-approved endpoint) ever responds with a 3xx redirect, the underlying OkHttp client transparently follows it to any destination, including internal-only services or the cloud metadata address 169.254.169.254, with no re-validation against the allowlist. A restricted, non-admin user who can create a lookup table data adapter or an event notification can exploit this to make the Graylog server issue requests to arbitrary internal destinations and read the response body directly through the lookup-table query API.

Details

HTTPJSONPathDataAdapter.doGet() performs the allowlist check only on the pre-redirect URL string:

// graylog2-server/src/main/java/org/graylog2/lookup/adapters/HTTPJSONPathDataAdapter.java:158-168
final String urlString = templateEngine.transform(config.url(), ImmutableMap.of("key", encodedKey));

if (!urlAllowlistService.isAllowlisted(urlString)) {
    LOG.error("Data adapter <{}>: URL <{}> is not allowlisted. Aborting lookup request.", name(), urlString);
    publishSystemNotificationForAllowlistFailure();
    setError(UrlNotAllowlistedException.forUrl(urlString));
    return getErrorResult();
}
...
try (final Response response = httpClient.newCall(request).execute()) {

httpClient is provided by OkHttpClientProvider, which never calls .followRedirects(false), so OkHttp's default (follow 3xx responses, including cross-host) applies. The response body is parsed and returned directly to the caller (parseBody, same file, lines 224-278), including whatever content the final, unvalidated redirect target returned.

DSVHTTPDataAdapter's file-fetch path is worse: it explicitly re-enables redirects:

// graylog2-server/src/main/java/org/graylog2/lookup/adapters/dsvhttp/HTTPFileRetriever.java:38-45
public HTTPFileRetriever(OkHttpClient httpClient) {
    this.client = httpClient.newBuilder()
            .followRedirects(true)
            .followSslRedirects(true)
            .build();
}

HTTPEventNotification/HTTPEventNotificationV2 (used for both saved and "test" webhook notifications) go through the same shared OkHttpClientProvider/ParameterizedHttpClientProvider, so the same bypass applies there too.

Reachability by non-admin users:

  • POST /system/lookup/adapters (create a data adapter) requires only RestPermissions.LOOKUP_TABLES_CREATE.
  • GET /system/lookup/adapters/{name}/query (trigger the lookup and read the result) requires only RestPermissions.LOOKUP_TABLES_READ.
  • POST /events/notifications/test (fire an unsaved HTTP notification test) requires only RestPermissions.EVENT_NOTIFICATIONS_CREATE.

None of these three permissions is admin-only; all are ordinary, independently assignable capabilities.

PoC

(available upon request)

Impact

Any user with the routine, non-admin lookuptables:create+lookuptables:read (or eventnotifications:create) permissions can turn a correctly-configured URL allowlist into a full SSRF primitive with response-body read access. In production deployments where any allowlisted endpoint is attacker-influenceable (an attacker-controlled webhook receiver used for testing, a third-party SaaS integration with an open redirect, or simply a DNS/routing change after the allowlist was written) this allows the Graylog server to be used to reach and read from internal-only services, including cloud instance metadata endpoints, effectively defeating the allowlist that exists specifically to prevent this class of attack.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions