9routers has Exposure of Sensitive Information and Unprotected Database Import/Export, Allowing Complete Credential Theft and Database Takeover
Summary
The /api/settings/database endpoint allows full database export (containing all credentials, API keys, OAuth tokens, and settings) and full database import (complete overwrite) without any authentication requirement beyond the ALWAYS_PROTECTED middleware check, which only validates JWT or CLI token. Combined with other vulnerabilities (e.g., default password, tunnel exposure), this enables complete database takeover.
Description
The endpoint /api/settings/database is listed in ALWAYS_PROTECTED in dashboardGuard.js (line 42), which requires a valid JWT token or CLI token. However, this protection is insufficient because:
- GET (Export): Returns the complete database including API keys (
keyfield inapiKeystable), OAuth tokens, and all provider credentials. Line 80 insrc/lib/db/index.js:apiKeys: db.all("SELECT * FROM apiKeys").map(...)— thekeyfield contains the plaintext API key value. - POST (Import): Accepts arbitrary JSON and performs a complete database wipe-and-replace in a transaction (lines 102-163 in
src/lib/db/index.js). This replaces all settings including the password hash, effectively allowing an attacker to set their own password. - The exported data includes
apiKeyswith their plaintextkeyvalues,providerConnectionswith all OAuth tokens, andsettingswith OIDC client secrets.
Evidence
File: src/app/api/settings/database/route.js
export async function GET() {
const payload = await exportDb();
return NextResponse.json(payload);
}export async function POST(request) {
const payload = await request.json();
await importDb(payload);
// ...
}
File: src/lib/db/index.js (lines 96-163)
export async function importDb(payload) {
db.transaction(() => {
// Wipe all tables
db.run(DELETE FROM settings);
db.run(DELETE FROM providerConnections);
db.run(DELETE FROM providerNodes);
db.run(DELETE FROM proxyPools);
db.run(DELETE FROM apiKeys);
db.run(DELETE FROM combos);
db.run(DELETE FROM kv WHERE scope IN (...));
// Then insert attacker-controlled data
// ...
});
}The exportDb function at line 80 exposes API key plaintext:
apiKeys: db.all(SELECT * FROM apiKeys).map((r) => ({
id: r.id, key: r.key, name: r.name, ...
})),Steps to Reproduce
- Authenticate with any valid JWT (e.g., using the default password "123456")
- Export:
curl -b auth_token= http://localhost:20128/api/settings/database - Observe: Full database dump with all credentials in plaintext
- Import malicious data:
curl -X POST -b auth_token= -H "Content-Type: application/json" -d '' http://localhost:20128/api/settings/database - All settings, passwords, API keys are now replaced with attacker-controlled values
Impact
- Confidentiality: Complete exposure of all stored secrets (API keys, OAuth tokens, OIDC client secrets)
- Integrity: Complete database replacement with attacker-controlled data
- Availability: Database wipe is possible by importing an empty database
- Scope Changed: Importing new settings affects all users and downstream services
Recommended Fix
- Require re-authentication for database export/import (not just an existing session)
- Mask/redact API keys in export (or require explicit opt-in for key export)
- Add confirmation step for import (require current password verification)
- Implement database backup before import
- Log all export/import operations with audit trail