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.
# 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
# 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
# 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
| Security Test | Method | Risk |
|---|---|---|
| Admin API user enumeration and impersonation | GET /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 tokens | Critical (requires admin token) |
| Webhook without secret — CI/CD trigger forgery | Webhooks without secret configuration accept any POST request; send forged push event JSON to webhook URL to trigger CI/CD pipeline runs without valid repository credentials | High |
| Server-side Git hook shell execution | Read /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 push | Critical |
| Private repository enumeration | GET /api/v1/repos/search?private=true — admin tokens return all private repositories including those in other organizations; reveals proprietary codebase structure and clone URLs | High |
| Actions secrets name enumeration | GET /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 injection | Medium |
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