Forgejo Security Testing: API Token, Webhook Secrets, Repository Access, and Git Hooks

Forgejo is the dominant community-maintained fork of Gitea and the most widely deployed self-hosted Git forge in privacy-conscious development environments. As the source code host, Forgejo is a high-value target: it holds all source code, all CI/CD secrets, and is the trust anchor for the development pipeline. Key assessment areas: Forgejo API tokens scoped to an admin account provide full user management and cross-repository access via the /api/v1/admin endpoints; webhook secrets protect CI/CD trigger integrity but can be brute-forced from the delivery body; server-side Git hooks stored at /data/forgejo/repositories/{owner}/{repo}.git/hooks/ execute shell commands as the git service user on every push; Forgejo admin can impersonate any user via the API using sudo parameter; and Forgejo Actions secrets stored in the database are available to all workflow steps. This guide covers systematic Forgejo security assessment.

Table of Contents

  1. API Token and Admin Access
  2. Webhook Secret and CI/CD Chain Security
  3. Server-Side Git Hooks
  4. Forgejo Security Hardening

API Token and Admin Access

# Forgejo REST API — authentication and admin access
FORGEJO_URL="https://forgejo.example.com"
TOKEN="your-forgejo-token"

# Verify token and get authenticated user info
curl -s "${FORGEJO_URL}/api/v1/user" \
  -H "Authorization: token ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'User: {d.get(\"login\")} admin={d.get(\"is_admin\")} email={d.get(\"email\")}')
print(f'Created: {d.get(\"created\")} last_login: {d.get(\"last_login\")}')
" 2>/dev/null

# Admin-only: enumerate all users on the instance
curl -s "${FORGEJO_URL}/api/v1/admin/users?limit=50" \
  -H "Authorization: token ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
users=json.load(sys.stdin)
print(f'Users: {len(users)}')
for u in users:
    print(f'  {u.get(\"login\")} admin={u.get(\"is_admin\")} email={u.get(\"email\")} 2fa={u.get(\"two_factor_authentication\")} active={u.get(\"active\")}')
" 2>/dev/null

# Search all repositories — includes private repos with admin token
curl -s "${FORGEJO_URL}/api/v1/repos/search?limit=50&private=true" \
  -H "Authorization: token ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
repos = d.get('data',[])
print(f'Repositories: {len(repos)}')
for r in repos:
    print(f'  {r.get(\"full_name\")} private={r.get(\"private\")} fork={r.get(\"fork\")}')
    print(f'    Clone: {r.get(\"clone_url\")}')
" 2>/dev/null

# Admin impersonation — access resources as another user
# Forgejo admin API tokens can add ?sudo=username to act as that user
curl -s "${FORGEJO_URL}/api/v1/user?sudo=targetuser" \
  -H "Authorization: token ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Acting as: {d.get(\"login\")} email={d.get(\"email\")}')
" 2>/dev/null

Webhook Secret and CI/CD Chain Security

# Forgejo webhooks — CI/CD trigger integrity and secret security
FORGEJO_URL="https://forgejo.example.com"
TOKEN="your-forgejo-token"

# Enumerate all webhooks for a repository — reveals delivery targets
ORG="myorg"
REPO="myrepo"
curl -s "${FORGEJO_URL}/api/v1/repos/${ORG}/${REPO}/hooks" \
  -H "Authorization: token ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
hooks=json.load(sys.stdin)
print(f'Webhooks: {len(hooks)}')
for h in hooks:
    config = h.get('config',{})
    print(f'  [{h.get(\"id\")}] {config.get(\"url\")} active={h.get(\"active\")} events={h.get(\"events\")}')
    if config.get('secret'):
        print(f'    Has secret: YES (HMAC-SHA256 signature verification)')
    else:
        print(f'    Has secret: NO — webhook can be forged without signature')
" 2>/dev/null

# Get recent webhook deliveries — may reveal response data from CI systems
curl -s "${FORGEJO_URL}/api/v1/repos/${ORG}/${REPO}/hooks/1/deliveries" \
  -H "Authorization: token ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
deliveries=json.load(sys.stdin)
print(f'Recent deliveries: {len(deliveries)}')
for d in deliveries[:5]:
    print(f'  [{d.get(\"id\")}] event={d.get(\"event\")} status={d.get(\"response_status\")} duration={d.get(\"duration\")}')
" 2>/dev/null

# Forgejo Actions secrets enumeration — accessible to all workflow steps
curl -s "${FORGEJO_URL}/api/v1/repos/${ORG}/${REPO}/actions/secrets" \
  -H "Authorization: token ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
try:
    d=json.load(sys.stdin)
    secrets = d.get('secrets',[])
    print(f'Actions secrets: {len(secrets)}')
    for s in secrets:
        print(f'  {s.get(\"name\")} updated={s.get(\"updated_at\")}')
except: print(sys.stdin.read()[:200])
" 2>/dev/null

Server-Side Git Hooks

# Forgejo server-side Git hooks — shell execution on push events
# Hooks stored at /data/forgejo/repositories/{owner}/{repo}.git/hooks/
# or the equivalent path in the Forgejo data directory

FORGEJO_DATA="/data/forgejo/repositories"  # adjust to actual data path

# Find all repository hooks — pre-receive and post-receive hooks execute as git user
find "${FORGEJO_DATA}" -name "pre-receive" -o -name "post-receive" -o -name "update" 2>/dev/null | \
while read hook; do
    echo "=== Hook: ${hook} ==="
    cat "${hook}" 2>/dev/null
done

# Check for custom hooks directory
# Forgejo supports custom hooks via the GITEA_CUSTOM/custom/hooks/ directory
ls -la /data/forgejo/custom/hooks/ 2>/dev/null

# Forgejo admin Git hooks — global hooks applied to all repositories
# Set via Forgejo admin panel: Site Administration > Git Repositories > Git Hooks
# These hooks execute shell commands as the forgejo/git service user on every push
# Malicious hooks can exfiltrate code, capture credentials from push data, or execute arbitrary commands

# Via API — get Git hooks for a repository (admin only)
curl -s "${FORGEJO_URL}/api/v1/repos/${ORG}/${REPO}/hooks/git" \
  -H "Authorization: token ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
hooks=json.load(sys.stdin)
for h in hooks:
    print(f'Git hook: {h.get(\"name\")} is_active={h.get(\"is_active\")}')
    content = h.get('content','')
    if content.strip():
        print(f'  Content: {content[:200]}')
" 2>/dev/null

Forgejo Security Hardening

Forgejo Security Hardening Checklist:
Security TestMethodRisk
Admin API user enumeration and impersonationGET /api/v1/admin/users — lists all users with 2FA status and admin flag; GET /api/v1/user?sudo=username with admin token impersonates target user and accesses their repositories and tokensCritical (requires admin token)
Webhook without secret — CI/CD trigger forgeryWebhooks without secret configuration accept any POST request; send forged push event JSON to webhook URL to trigger CI/CD pipeline runs without valid repository credentialsHigh
Server-side Git hook shell executionRead /data/forgejo/repositories/{owner}/{repo}.git/hooks/ — pre-receive/post-receive scripts execute as git service user; malicious hooks exfiltrate code or run arbitrary commands on every pushCritical
Private repository enumerationGET /api/v1/repos/search?private=true — admin tokens return all private repositories including those in other organizations; reveals proprietary codebase structure and clone URLsHigh
Actions secrets name enumerationGET /api/v1/repos/{org}/{repo}/actions/secrets — lists secret names accessible to Actions workflows; secret values not returned but names guide targeted exfiltration via workflow injectionMedium

Automate Forgejo Security Testing

Ironimo tests Forgejo deployments for admin API token scope and user enumeration, webhook secret configuration audit across all repositories, server-side Git hook content review, admin user 2FA enforcement status, Actions secret enumeration and workflow injection risk, organization member permission mapping, app.ini credential exposure, and private repository access control assessment.

Start free scan