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-Typeheaders likeapplication/json,application/xml.- Presence of
Authorizationor custom API key headers (e.g.,X-API-Key).
- Paths containing
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
- Header Stripping: Remove all
Authorization,X-API-Key, or other custom authentication headers. This creates the "unauthenticated" request. - Request Resending: Utilize
IBurpExtenderCallbacks.makeHttpRequestto 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. - Response Comparison:
- Status Code: A primary indicator. If an authenticated request returns
200 OKand the unauthenticated version also returns200 OK(instead of401 Unauthorizedor403 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.
- Status Code: A primary indicator. If an authenticated request returns
- 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
IScanIssueinterface.
# ... 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 andHttpOnlyflags. - False Positives: APIs often have public endpoints (e.g., login, registration, public data) that are legitimately unauthenticated. The current
is_api_requestheuristic 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.