Every layer documented. Every default explained. If you find a security issue, the disclosure process is at the bottom of this page.
Browse the layers below. Defaults marked with a green dot are enabled automatically on first install — no configuration required.
Passwords are hashed with PHP's password_hash() using bcrypt at cost factor 10. Plain-text passwords are never logged, never stored, never sent in URLs. The hash includes a per-password salt, so identical passwords produce different hashes.
Time-based one-time password (RFC 6238). 6-digit codes, 30-second window, ±1 step tolerance for clock drift. Compatible with Google Authenticator, Authy, 1Password, Bitwarden. Enroll via QR code; backup codes generated at setup.
Every state-changing request — POST, PUT, DELETE, AJAX mutations — is validated against a per-session CSRF token. Tokens rotate on login and on privilege change. GET requests are never used to mutate state.
Failed login attempts are counted per username and per IP. After 5 failures within 15 minutes, the account is locked for 15 minutes. 2FA failures count toward the same threshold. All thresholds configurable in config.php.
Optional. Restrict access by IP range using CIDR notation (e.g. 10.0.0.0/8, 203.0.113.5/32). Checked before authentication. Supports both IPv4 and IPv6. Especially useful for production deployments behind a VPN.
When enabled, all write statements (INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, etc.) are rejected at the application layer. Multi-statement input is split and each statement is checked — so USE db; DROP TABLE x; is blocked even though it starts with a SELECT-equivalent statement.
User-provided values never enter SQL via string concatenation. PDO prepared statements are used for every parameterized query — including the "search across all tables" feature, where the search term is bound, not interpolated. Schema identifiers (table/column names) are validated against the live schema before use, never trusted from input.
Default response headers include X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: strict-origin-when-cross-origin, and a restrictive Permissions-Policy. HSTS available via .htaccess once you're on HTTPS.
Session cookies are HttpOnly, SameSite=Lax, and Secure when served over HTTPS. Session IDs rotate on login and privilege change. Idle timeout 1 hour, absolute timeout 12 hours — both configurable.
Specific databases or schemas can be hidden from the UI entirely. Hidden databases don't appear in the sidebar, autocomplete, URL access, or exports — even if the underlying MySQL user has privileges. Useful for keeping mysql, information_schema, or sensitive customer DBs out of view.
Every executed query is logged with timestamp, username, source IP, target database, execution time, and success/error state. Logs rotate weekly by default, retained 90 days. Failed authentication attempts logged separately. Plain-text log files — grep-friendly, no database needed.
An .htaccess in includes/, pages/, and logs/ denies direct HTTP access. config.php is generated outside the document root pattern when possible. No PHP file lists its contents on direct access — every entry point requires authentication.
Please report it privately so it can be fixed before being disclosed publicly.
The fastest, most secure channel is GitHub's private security advisories. This lets the maintainer respond, investigate, and patch before details become public.
If for some reason you can't use GitHub, the disclosure email is listed in SECURITY.md.
--version or commit hash)Don't include sample data containing real credentials or PII.
Researchers who report valid vulnerabilities are credited in the security advisory and the changelog, unless they prefer to remain anonymous.
No advisories yet — Ledger v1.0.0-beta has no known vulnerabilities at time of release. Help keep it that way.
Even with 12 layers, no software is unbreakable. Keep your install behind HTTPS, restrict access at the network layer, and update when patches ship.