Introduction
In the plugins/catalog/bitbucket.py file, a critical security flaw was discovered in the get_bitbucket_contents() function that handles Bitbucket repository scanning. While the initial API request correctly uses HTTPS (https://api.bitbucket.org/2.0/repositories/...), the pagination logic at line 71 blindly trusted URLs returned by the Bitbucket API without validating they use HTTPS.
This matters because the code sends HTTP Basic Authentication credentials with every request:
response = requests.get(api_url, auth=HTTPBasicAuth(repo_owner, repo.token), proxies=PROXY)
If an attacker could manipulate the next URL in a Bitbucket API response—through a compromised proxy, DNS poisoning, or a man-in-the-middle position—they could redirect the connector to an HTTP endpoint and capture the OAuth token in plaintext.
The Vulnerability Explained
The vulnerable code pattern appears in the pagination loop starting at line 69:
# VULNERABLE CODE
while "next" in response.json():
api_url = response.json()["next"]
if repo.token:
response = requests.get(api_url, auth=HTTPBasicAuth(repo_owner, repo.token), proxies=PROXY)
else:
response = requests.get(api_url, proxies=PROXY)
What Makes This Dangerous
The api_url variable is assigned directly from response.json()["next"]—data that comes from an external API response. This creates a Server-Side Request Forgery (SSRF) adjacent pattern where the application trusts external input to determine where to send authenticated requests.
Attack Scenario
Consider this attack chain specific to this Bitbucket connector:
-
Initial Setup: An attacker positions themselves as a network intermediary (corporate proxy, compromised DNS, malicious WiFi)
-
Interception: When the connector makes its first legitimate HTTPS request to
api.bitbucket.org, the attacker intercepts the response -
Payload Injection: The attacker modifies the JSON response to include:
json { "values": [...], "next": "http://attacker-controlled.com/capture" } -
Credential Theft: The connector follows this URL, sending the
repo.tokenvia HTTP Basic Auth over an unencrypted connection -
Token Capture: The attacker's server receives the base64-encoded credentials in the
Authorizationheader
Additional Risk: Missing Timeouts
The original code also lacked request timeouts, meaning a slow or unresponsive server could hang the connector indefinitely—a potential denial-of-service vector.
The Fix
The fix implements two critical security improvements in plugins/catalog/bitbucket.py:
1. HTTPS Enforcement for Pagination URLs (Lines 71-74)
Before:
while "next" in response.json():
api_url = response.json()["next"]
if repo.token:
response = requests.get(api_url, auth=HTTPBasicAuth(repo_owner, repo.token), proxies=PROXY)
After:
while "next" in response.json():
api_url = response.json()["next"]
if not api_url.startswith("https://"):
add_error_notification(f"Bitbucket connector: refusing to follow non-HTTPS pagination URL")
break
if repo.token:
response = requests.get(api_url, auth=HTTPBasicAuth(repo_owner, repo.token), proxies=PROXY, timeout=30)
This validation ensures that:
- Only HTTPS URLs are followed for pagination
- An error is logged when a non-HTTPS URL is encountered
- The pagination loop breaks safely rather than exposing credentials
2. Request Timeouts (Lines 55-56, 76-79)
All four requests.get() calls now include timeout=30:
response = requests.get(api_url, auth=HTTPBasicAuth(repo_owner, repo.token), proxies=PROXY, timeout=30)
This prevents indefinite hangs and limits the window for timing-based attacks.
Why This Fix Works
The fix addresses the root cause by validating the URL scheme before sending credentials, rather than trusting external input. The startswith("https://") check is simple but effective because:
- It's case-sensitive and explicit (won't match
HTTPS://orhTTpS://, though these are rare) - It runs before any network request is made
- It fails safely by breaking the loop and logging an error
Prevention & Best Practices
For Python Developers Using requests
- Always validate external URLs before following them:
```python
from urllib.parse import urlparse
def is_safe_url(url, allowed_hosts=None):
parsed = urlparse(url)
if parsed.scheme != 'https':
return False
if allowed_hosts and parsed.netloc not in allowed_hosts:
return False
return True
```
-
Use session objects with controlled redirects:
python session = requests.Session() session.max_redirects = 5 response = session.get(url, allow_redirects=False) # Handle redirects manually -
Always set timeouts on network requests:
```python
# Good: explicit timeout
requests.get(url, timeout=(5, 30)) # (connect timeout, read timeout)
# Bad: no timeout (can hang forever)
requests.get(url)
```
- Consider using
requests-toolbeltfor advanced URL validation
Security Standards
- OWASP: This vulnerability relates to Sensitive Data Exposure and Server-Side Request Forgery
- CWE-319: Cleartext Transmission of Sensitive Information
- CWE-918: Server-Side Request Forgery (related, as external URLs control request destination)
Key Takeaways
- Never trust pagination URLs from API responses—always validate the scheme before following them with credentials
- The
get_bitbucket_contents()function now validates all pagination URLs start withhttps://before sending OAuth tokens - HTTP Basic Authentication sends credentials with every request—if even one request goes to HTTP, credentials are exposed
- Request timeouts are a security control, not just a reliability feature—they limit attack windows
- Defense in depth matters: even though the initial URL was HTTPS, the pagination URLs needed separate validation
How Orbis AppSec Detected This
- Source: The
response.json()["next"]pagination URL from Bitbucket API responses at line 71 - Sink:
requests.get(api_url, auth=HTTPBasicAuth(...))at lines 73 and 76, where credentials are sent - Missing control: No validation that pagination URLs use HTTPS before sending authentication credentials
- CWE: CWE-319 (Cleartext Transmission of Sensitive Information)
- Fix: Added HTTPS prefix validation before following pagination URLs and 30-second timeouts on all requests
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
This vulnerability demonstrates a common but dangerous pattern: trusting external data to control where authenticated requests are sent. While the Bitbucket connector correctly used HTTPS for its initial API call, the pagination logic created a gap where a malicious or compromised response could redirect credentials over HTTP.
The fix is straightforward—validate URL schemes before following them—but the implications of missing this check are severe: complete credential compromise. When building integrations with external APIs, always treat response data as untrusted input, especially when that data influences subsequent authenticated requests.