Developing a Burp Suite Extension for Automated API Authentication Bypass Detection

Developing a Burp Suite extension for automated API authentication bypass detection directly addresses a critical gap in many web application penetration tests: the often-manual and time-consuming process of verifying authentication controls across numerous API endpoints. Automating this verification allows testers to rapidly identify whether specific API calls, designed for authenticated users, can be successfully invoked or return sensitive data when stripped of their authentication tokens.

Burp Extensibility: The Core Framework

Burp Suite's power is significantly enhanced by its extensibility, primarily through the Extender API. This API allows for custom functionality development using Python (via Jython), Ruby (via JRuby), or Java. For rapid prototyping and ease of development, Python is often the preferred choice among pentesters. The fundamental interface for any Burp extension is IBurpExtender, which serves as the entry point for Burp to load and interact with your custom code.

Setting Up Your Development Environment

  • Download and install Jython standalone JAR from the official jython.org download page. Jython 2.7.4 is the current stable version.
  • In Burp Suite, navigate to Extender > Options > Python Environment.
  • Set the "Location of Jython standalone JAR file" to the path of your downloaded JAR.
  • Add the folder containing your Python extension scripts to the "Folder for loading extensions" list.

Once configured, Burp can load Python scripts that implement the IBurpExtender interface. The primary callback method is registerExtenderCallbacks, providing access to Burp's core functionality via the callbacks object and utility functions through the helpers object.


from burp import IBurpExtender, IHttpListener, IScanIssue

class BurpExtender(IBurpExtender, IHttpListener):
    EXTENSION_NAME = "API Auth Bypass Detector"

    def registerExtenderCallbacks(self, callbacks):
        self._callbacks = callbacks
        self._helpers = callbacks.getHelpers() # Get helpers instance
        callbacks.setExtensionName(self.EXTENSION_NAME)
        callbacks.registerHttpListener(self)
        callbacks.issueAlert(f"[{self.EXTENSION_NAME}] loaded successfully.")
        print(f"{self.EXTENSION_NAME} loaded successfully.")
        
    # Other methods will follow...

Identifying API Requests for Analysis

The core of our detection logic resides in the processHttpMessage method, part of the IHttpListener interface. This method is invoked for every HTTP request and response processed by Burp. To avoid unnecessary processing, we must filter for relevant API requests and ensure they are within the defined scope of our test.

Filtering Criteria

  • Scope Check: Always prioritize requests within Burp's defined scope to prevent out-of-scope traffic analysis.
  • Request Type: We are primarily interested in analyzing requests, not responses, as our goal is to modify and resend them.
  • API Heuristics: While not definitive, common indicators of API requests include:
    • Paths containing /api/, /v1/, /rest/.
    • Content-Type headers like application/json, application/xml.
    • Presence of Authorization or custom API key headers (e.g., X-API-Key).

    def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo):
        # Only process outgoing requests from proxy, scanner, etc.
        # TOOL_PROXY, TOOL_SCANNER, TOOL_REPEATER, etc.
        if not messageIsRequest or toolFlag == self._callbacks.TOOL_EXTENDER: 
            return # Skip if it's a response or a request generated by our own extension

        if not self._callbacks.isInScope(self._helpers.analyzeRequest(messageInfo).getUrl()):
            return # Skip if out of scope

        request_info = self._helpers.analyzeRequest(messageInfo)
        headers = request_info.getHeaders()
        url = request_info.getUrl()

        # Simple heuristic for API requests: check URL path or common API headers
        is_api_request = False
        if "/api/" in url.getPath().lower() or "/v1/" in url.getPath().lower():
            is_api_request = True
        
        # Check for common authentication headers as a stronger indicator
        auth_headers = [h for h in headers if h.lower().startswith('authorization') or h.lower().startswith('x-api-key')]
        
        if not is_api_request and not auth_headers:
            return # Not likely an API request with auth, skip.
        
        if not auth_headers:
            # If it's an API request but has no auth header, it's already unauthenticated. Skip bypass test.
            return
        
        # At this point, we have an in-scope API request that contains authentication headers.
        # Proceed with bypass attempt.
        self._callbacks.issueAlert(f"[{self.EXTENSION_NAME}] Testing auth bypass for: {url.toString()}")
        # ... (rest of the bypass logic)

Automated Authentication Bypass Strategy

The core strategy involves taking an authenticated API request, systematically removing its authentication credentials, resending the modified request, and then analyzing the resulting response. If the unauthenticated request yields a similar, successful, or information-rich response compared to the original authenticated request, a bypass is likely present.

Detection Heuristics

  1. Header Stripping: Remove all Authorization, X-API-Key, or other custom authentication headers. This creates the "unauthenticated" request.
  2. Request Resending: Utilize IBurpExtenderCallbacks.makeHttpRequest to send the modified request. This function performs a new HTTP request without interfering with the original traffic flow in Burp's proxy. The method requires the host, port, boolean for HTTPS, and the raw request bytes.
  3. Response Comparison:
    • Status Code: A primary indicator. If an authenticated request returns 200 OK and the unauthenticated version also returns 200 OK (instead of 401 Unauthorized or 403 Forbidden), it's a strong bypass signal.
    • Response Length/Body Similarity: Compare the byte length or content of the response bodies. Significant similarity (e.g., less than a 10% difference in length) when sensitive data is expected could indicate a bypass.
    • Sensitive Data Presence: Look for specific keywords or data patterns (e.g., user_id, account balances, PII) in the unauthenticated response that were present in the authenticated one.
  4. Reporting: If a potential bypass is detected, report it as a scan issue within Burp, providing both the original and unauthenticated request/response pairs for easy analysis via the IScanIssue interface.

# ... inside processHttpMessage after filtering ...

        request_body = messageInfo.getRequest()[request_info.getBodyOffset():]

        # 1. Create a modified request without authentication headers
        modified_headers = [h for h in headers if not (h.lower().startswith('authorization') or h.lower().startswith('x-api-key'))]
        modified_request = self._helpers.buildHttpMessage(modified_headers, request_body) #

        # 2. Send the modified request
        http_service = messageInfo.getHttpService() # Get details about the service
        original_url = request_info.getUrl()
        
        try:
            # makeHttpRequest (host, port, useHttps, request_bytes)
            unauth_response_request = self._callbacks.makeHttpRequest(
                http_service.getHost(), 
                http_service.getPort(), 
                original_url.getProtocol().lower() == "https", # Use getProtocol from URL or check httpService
                modified_request
            )
            
            original_response_info = self._helpers.analyzeResponse(messageInfo.getResponse()) #
            unauth_analyzed_response = self._helpers.analyzeResponse(unauth_response_request.getResponse())

            original_status_code = original_response_info.getStatusCode()
            unauth_status_code = unauth_analyzed_response.getStatusCode()

            # Retrieve response bodies as strings for comparison
            original_response_body = self._helpers.bytesToString(messageInfo.getResponse()[original_response_info.getBodyOffset():]) #
            unauth_response_body = self._helpers.bytesToString(unauth_response_request.getResponse()[unauth_analyzed_response.getBodyOffset():])

            # Simple detection logic
            # Scenario 1: Original successful, unauth also successful (potential bypass)
            if 200 <= original_status_code < 300 and 200 <= unauth_status_code < 300:
                length_diff_percentage = abs(len(original_response_body) - len(unauth_response_body)) * 100.0 / (len(original_response_body) + 1) # +1 to avoid div by zero
                
                # If response bodies are very similar, likely a bypass
                if length_diff_percentage < 10: # Threshold: less than 10% difference
                    self._callbacks.addScanIssue(AuthBypassScanIssue(
                        messageInfo.getHttpService(),
                        request_info.getUrl(),
                        [messageInfo, unauth_response_request], 
                        self.EXTENSION_NAME,
                        f"Potential API Authentication Bypass: Unauthenticated request returned similar data (Status {unauth_status_code}). Original: {original_status_code}.",
                        "High",
                        "Firm"
                    ))
                    self._callbacks.issueAlert(f"[{self.EXTENSION_NAME}] *VULNERABLE*: Auth bypass (response length/similarity) detected for {url.toString()}")
                    return # Issue found, no need for further checks on this request

                # Deeper check: if status codes are similar but lengths differ, look for sensitive keywords
                common_sensitive_keywords = ["user_id", "email", "account_balance", "session_token"] # Example keywords
                if any(keyword in original_response_body and keyword in unauth_response_body for keyword in common_sensitive_keywords):
                     self._callbacks.addScanIssue(AuthBypassScanIssue(
                        messageInfo.getHttpService(),
                        request_info.getUrl(),
                        [messageInfo, unauth_response_request],
                        self.EXTENSION_NAME,
                        f"Potential API Authentication Bypass: Sensitive data still present in unauthenticated response (Status {unauth_status_code}). Original: {original_status_code}.",
                        "High",
                        "Firm"
                    ))
                     self._callbacks.issueAlert(f"[{self.EXTENSION_NAME}] *VULNERABLE*: Auth bypass (sensitive data) detected for {url.toString()}")
                     return

            # Scenario 2: Original successful, unauth gets an unexpected status code (not 401/403)
            elif 200 <= original_status_code < 300 and unauth_status_code not in:
                self._callbacks.addScanIssue(AuthBypassScanIssue(
                    messageInfo.getHttpService(),
                    request_info.getUrl(),
                    [messageInfo, unauth_response_request],
                    self.EXTENSION_NAME,
                    f"Potential API Authentication Bypass: Unauthenticated request returned unexpected status code {unauth_status_code}. Expected 401/403. Original: {original_status_code}.",
                    "Medium",
                    "Tentative"
                ))
                self._callbacks.issueAlert(f"[{self.EXTENSION_NAME}] Possible bypass: Unexpected unauth status code {unauth_status_code} for {url.toString()}")
                return
            
        except Exception as e:
            self._callbacks.issueAlert(f"[{self.EXTENSION_NAME}] Error processing request for {url.toString()}: {e}")

# Custom Scan Issue class implementation
class AuthBypassScanIssue(IScanIssue):
    def __init__(self, httpService, url, httpMessages, name, detail, severity, confidence):
        self._httpService = httpService
        self._url = url
        self._httpMessages = httpMessages # List of IHttpRequestResponse objects
        self._name = name
        self._detail = detail
        self._severity = severity
        self._confidence = confidence

    def getUrl(self):
        return self._url

    def getIssueName(self):
        return self._name

    def getIssueType(self):
        return 0 # Custom issue type, can be a specific int if desired

    def getSeverity(self):
        return self._severity

    def getConfidence(self):
        return self._confidence

    def getIssueBackground(self):
        return """
        

This issue identifies a potential API authentication bypass vulnerability. An authenticated API request was intercepted, and then re-sent with its authentication credentials (e.g., Authorization header, X-API-Key) stripped. The unauthenticated request resulted in a response that was either successful, similar in content to the authenticated response, or returned sensitive information, indicating that the API endpoint may not be properly enforcing authentication.

Attackers could exploit this to access sensitive data, perform unauthorized actions, or enumerate resources without needing valid user credentials.

""" def getRemediationBackground(self): return """

All API endpoints that require authentication must strictly enforce it. Implement robust server-side authentication and authorization checks for every request to a protected resource. Ensure that:

  • Requests without valid authentication tokens are rejected with appropriate HTTP status codes (e.g., 401 Unauthorized, 403 Forbidden).
  • Error responses for unauthenticated requests do not leak sensitive information.
  • Input validation and authorization policies are applied consistently across all API versions and endpoints.

""" def getIssueDetail(self): return self._detail + "

The issue provides two HTTP messages: the first is the original authenticated request/response, and the second is the unauthenticated request/response generated by the extension for comparison." def getHttpMessages(self): # Burp expects a list of IHttpRequestResponse objects return self._httpMessages def getHttpService(self): return self._httpService

Real-World Considerations and Refinements

While the presented script provides a functional foundation, real-world API testing demands further refinement. Consider the following:

  • Cookie-based Authentication: The current script primarily targets header-based authentication. For cookie-based sessions, the logic would need to identify and remove session cookies (e.g., JSESSIONID, PHPSESSID, connect.sid) before resending. This can be more complex due to cookie scope and HttpOnly flags.
  • False Positives: APIs often have public endpoints (e.g., login, registration, public data) that are legitimately unauthenticated. The current is_api_request heuristic is basic; a more sophisticated approach might involve maintaining a whitelist/blacklist of known public endpoints or leveraging Burp's site map for context.
  • Performance: Sending an additional request for every in-scope authenticated API call can impact Burp's performance, especially during heavy browsing or scanning. Implementing rate limiting, or a user-configurable option to enable/disable the extension or target specific endpoints, would be beneficial.
  • Deep Content Analysis: Beyond simple length checks, advanced detection could involve:
    • JSON/XML parsing and diffing relevant fields.
    • Machine learning to identify "sensitive" responses based on data structure or content.
    • Checking for specific error messages (e.g., generic server errors vs. explicit authentication failures).
  • Reporting Granularity: The current issue reporting is effective, but an extension could offer more detail, such as highlighting the exact differences in response bodies or providing a confidence score based on multiple heuristics.

Example Burp Scanner Output

When a potential bypass is detected and reported, it will appear in the Burp Suite's "Target" tab under "Issues" or in the "Scanner" tab (if reporting as a Scanner issue). The entry might look similar to this:


Issue: Potential API Authentication Bypass: Unauthenticated request returned similar data (Status 200). Original: 200.
URL: https://example.com/api/v1/users/profile/123
Severity: High
Confidence: Firm
Detail: The extension detected a potential API authentication bypass vulnerability. An authenticated API request was intercepted for /api/v1/users/profile/123, and then re-sent with its Authorization header stripped. The unauthenticated request resulted in a 200 OK response with a response body very similar in length and content to the original authenticated response, indicating that sensitive user profile information may be accessible without proper authentication.

Background: This issue identifies a potential API authentication bypass vulnerability... (full background from IScanIssue)
Remediation: All API endpoints that require authentication must strictly enforce it... (full remediation from IScanIssue)
HTTP messages:
    1. Original Authenticated Request/Response (GET /api/v1/users/profile/123 HTTP/1.1 ...)
    2. Unauthenticated Request/Response (GET /api/v1/users/profile/123 HTTP/1.1 ...)

The "HTTP messages" section is crucial for manual verification, allowing a pentester to quickly compare the original request (with valid tokens) and the unauthenticated re-sent request, along with their respective responses, directly within Burp. This facilitates understanding the nature and extent of the bypass.