Security

Twelve layers,
enabled by default.

Every layer documented. Every default explained. If you find a security issue, the disclosure process is at the bottom of this page.

The threat model. Ledger is a web-based admin tool with privileged database access. The attacker is anyone with network reach to the install — including authenticated users acting maliciously. Every layer below addresses a specific attack vector that has historically affected web database tools.

The chain

Each layer addresses a specific attack.

Browse the layers below. Defaults marked with a green dot are enabled automatically on first install — no configuration required.

01

Bcrypt password hashing

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.

Default on $2y$10$ PASSWORD_BCRYPT
02

TOTP 2FA per user

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.

RFC 6238 Opt-in per user QR enrollment
03

CSRF tokens

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.

Default on Per-session Auto-rotating
04

Brute-force lockout

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.

Default on 5 attempts / 15 min User + IP tracked
05

IP whitelist (CIDR)

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.

Opt-in CIDR v4 + v6 Pre-auth check
06

Read-only mode

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.

Opt-in Multi-statement aware Per-user or global
07

Prepared statements throughout

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 on PDO prepared No string interp
08

Security headers

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.

Default on CSP-ready HSTS optional
09

Session hardening

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.

Default on HttpOnly SameSite=Lax Rotates on login
10

Hidden databases

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.

Opt-in Glob patterns Full UI scrub
11

Query audit log

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.

Default on logs/audit/ 90 day retention
12

Filesystem isolation

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.

Default on .htaccess No directory listing
Responsible disclosure

Found a vulnerability?

Please report it privately so it can be fixed before being disclosed publicly.

How to report

The fastest, most secure channel is GitHub's private security advisories. This lets the maintainer respond, investigate, and patch before details become public.

Report on GitHub

If for some reason you can't use GitHub, the disclosure email is listed in SECURITY.md.

What to include

  • Affected version (output of --version or commit hash)
  • PHP and MySQL/MariaDB versions
  • Steps to reproduce, ideally with a minimal proof-of-concept
  • The actual impact — what an attacker could do
  • Your suggested fix, if you have one

Don't include sample data containing real credentials or PII.

What to expect

  • Within 48 hours: acknowledgment that the report was received.
  • Within 7 days: initial assessment and severity rating.
  • Patch timeline: typically 7–30 days depending on complexity, communicated up-front.
  • Public disclosure: coordinated with you, after the patch ships.

Hall of fame

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.

Security is a process.

Even with 12 layers, no software is unbreakable. Keep your install behind HTTPS, restrict access at the network layer, and update when patches ship.

Install Ledger Full SECURITY.md