Standard Notes is a widely deployed self-hosted end-to-end encrypted notes server — it is used by privacy-conscious individuals and security professionals who want encrypted note synchronization under their own control. Key assessment areas: SECRET_KEY_BASE is the Rails session signing key; JWT access tokens authenticate all API calls; while note content is E2E encrypted by the client, session token compromise enables account takeover including access to encrypted blobs, session revocation across all devices, and account manipulation; and the database stores user account metadata and session tokens. This guide covers systematic Standard Notes security assessment.
# Standard Notes — authentication API and credential testing
SN_URL="https://notes.example.com"
# Standard Notes uses a Rails API for account management
# Authentication returns JWT access token + refresh token
# Test login with known credentials
curl -s -X POST "${SN_URL}/v1/auth/sign_in" \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"user-password","api":20200115}' \
2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
if 'session' in d:
sess = d.get('session',{})
user = d.get('user',{})
print(f'OK user_uuid={user.get(\"uuid\",\"\")[:8]} email={user.get(\"email\")}')
print(f' access_token={sess.get(\"access_token\",\"\")[:30]}...')
print(f' refresh_token={sess.get(\"refresh_token\",\"\")[:30]}...')
print(f' access_expiration={sess.get(\"access_expiration\")}')
elif 'error' in d:
print(f'FAIL: {d.get(\"error\",{}).get(\"message\",\"\")}')
else:
print(f'Response: {list(d.keys())}')
" 2>/dev/null
# Check registration endpoint — may be open allowing account creation
curl -s -X POST "${SN_URL}/v1/users" \
-H "Content-Type: application/json" \
-d '{"email":"test@attacker.com","password":"testpassword123","api":20200115,
"pw_nonce":"random_nonce","version":"004"}' \
2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
if 'session' in d:
print('OPEN REGISTRATION: Account created successfully')
print(f' UUID: {d.get(\"user\",{}).get(\"uuid\")}')
elif 'error' in d:
print(f'Registration restricted: {d.get(\"error\",{}).get(\"message\",\"\")}')
" 2>/dev/null
# Standard Notes session token and account access
SN_URL="https://notes.example.com"
ACCESS_TOKEN="your-sn-access-token"
# Get user account information
curl -s "${SN_URL}/v1/users/{user-uuid}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
user = d.get('user',d)
print(f'Email: {user.get(\"email\")}')
print(f'UUID: {user.get(\"uuid\")}')
print(f'Created: {user.get(\"created_at\")}')
print(f'Storage used: {user.get(\"bytes_used\",0)//1024//1024}MB / quota')
" 2>/dev/null
# List all active sessions (access to all logged-in devices)
curl -s "${SN_URL}/v1/sessions" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
sessions = d.get('sessions',[])
print(f'Active sessions: {len(sessions)}')
for s in sessions:
print(f' [{str(s.get(\"uuid\",\"\"))[:8]}] {s.get(\"user_agent\",\"\")[:60]}')
print(f' updated={str(s.get(\"updated_at\",\"\"))[:19]} current={s.get(\"current\")}')
" 2>/dev/null
# Can revoke other sessions — log out all user devices
# Get all items (encrypted note blobs)
curl -s "${SN_URL}/v1/items/all" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
items = d.get('retrieved_items',[])
print(f'Encrypted items: {len(items)}')
for item in items[:10]:
print(f' [{item.get(\"uuid\",\"\")[:8]}] content_type={item.get(\"content_type\")}')
# content is encrypted — requires user encryption key to decrypt
content_len = len(str(item.get('content','')))
print(f' encrypted_content_length={content_len} bytes updated={str(item.get(\"updated_at\",\"\"))[:10]}')
" 2>/dev/null
# Note: content is E2E encrypted client-side — server sees ciphertext only
# Standard Notes SECRET_KEY_BASE and database extraction
# Environment configuration
cat /var/www/standardnotes/.env /app/.env 2>/dev/null | \
grep -E "SECRET_KEY_BASE|DB_|RAILS_ENV|DISABLE_USER_REGISTRATION|PSEUDO_KEY_PARAMS"
# SECRET_KEY_BASE — Rails session signing key
# DB_HOST, DB_USERNAME, DB_PASSWORD, DB_DATABASE
# DISABLE_USER_REGISTRATION — controls open registration
# PSEUDO_KEY_PARAMS — server-side auth parameters
# Docker environment inspection
docker inspect standard-notes 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 ['SECRET','PASSWORD','DB_','RAILS','JWT','PSEUDO']):
print(e)
" 2>/dev/null
# MySQL/PostgreSQL — Standard Notes database
mysql -h localhost -u standardnotes -pPASSWORD standardnotes 2>/dev/null << 'EOF'
-- All user accounts
SELECT u.uuid, u.email,
u.pw_cost, u.pw_nonce, u.version,
u.created_at, u.updated_at,
u.bytes_used
FROM users u
ORDER BY u.created_at ASC
LIMIT 20;
-- All active sessions (bearer tokens for account access)
SELECT s.uuid, s.user_uuid,
s.hashed_access_token,
s.access_expiration,
s.refresh_expiration,
s.user_agent, s.created_at
FROM sessions s
ORDER BY s.created_at DESC
LIMIT 20;
-- hashed_access_token: SHA256 hash of bearer token
-- refresh_expiration: long-lived token for session renewal
-- All encrypted items (note blobs)
SELECT i.uuid, i.user_uuid,
i.content_type,
LENGTH(i.content) as content_size,
i.created_at, i.updated_at
FROM items i
WHERE i.deleted = 0
ORDER BY i.updated_at DESC
LIMIT 20;
-- content: AES-256 encrypted by client — server cannot read plaintext
EOF
| Security Test | Method | Risk |
|---|---|---|
| Open registration account creation | POST /v1/users — if DISABLE_USER_REGISTRATION is not set, create accounts on private server; resource consumption; potential for multi-user data co-mingling depending on configuration | Medium |
| JWT access token session enumeration and revocation | GET /v1/sessions with valid token — list all active sessions across all devices; revoke specific sessions to log out user from devices; denial of service by revoking all sessions | High |
| Database session table token extraction | SELECT hashed_access_token FROM sessions — session token hashes; access tokens can be brute-forced or observed in transit; account takeover without password knowledge | High |
| SECRET_KEY_BASE extraction and Rails session forgery | Read .env SECRET_KEY_BASE — forge Rails session cookies for server admin interface access; broader than user API access — administrative operations on all accounts | High |
| Encrypted item blob bulk download | GET /v1/items/all — all user's encrypted note ciphertext; while content is E2E encrypted, offline cryptanalysis may be possible if user uses weak password; note count, creation dates, and content size are metadata that reveals usage patterns | Medium |
Ironimo tests Standard Notes deployments for open user registration endpoint access, JWT session token enumeration and cross-device revocation capability, database session table token extraction, SECRET_KEY_BASE Rails session forgery, encrypted item blob bulk download for offline cryptanalysis assessment, account metadata exposure (email, storage usage, note count), Docker SECRET_KEY_BASE/DB_PASSWORD exposure, authentication rate limiting assessment, password strength policy enforcement, and encrypted blob size analysis for content pattern inference.
Start free scan