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.
# 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
# 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
# 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
| Security Test | Method | Risk |
|---|---|---|
| Default shiori/shiori credential testing | POST /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 misconfiguration | Critical |
| Bookmark history privacy exposure | GET /api/v1/bookmarks with session token — complete browsing history including private bookmarks, tags, and timestamps; reveals research interests, reading patterns, and sensitive URL access history | High |
| Public bookmark enumeration without authentication | GET /api/v1/bookmarks?public=1 without credentials — all bookmarks marked public; may expose sensitive research topics or internal URLs marked public by mistake | Medium |
| SQLite bookmark table URL extraction | SELECT url FROM bookmark — complete URL history for all bookmarks; offline archive content if has_content=1; account table argon2 password hashes | High |
| Offline archive sensitive content access | GET /bookmark/{id}/archive — full HTML snapshots of bookmarked pages; may include content from behind authentication walls, paywalled articles, or sensitive pages captured at archive time | High |
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