Skip to content

Releases

Release v1.1.1-beta

Choose a tag to compare

@ClearanceClarence ClearanceClarence released this 29 Oct 19:23

Release v1.0.2-beta

Choose a tag to compare

@ClearanceClarence ClearanceClarence released this 12 Jun 09:49

Ledger 1.0.2-beta is a security and polish release. A full self-audit of the codebase turned up a handful of real issues — the most serious being that X-Forwarded-For was trusted as the client IP (spoofable, which bypassed the IP allowlist and brute-force lockout), that read-only mode could be slipped past via the EXPLAIN endpoint and comment-prefixed writes, and that the Saved Queries feature was outright broken by a dropped function signature. All fixed here.

On top of the security work: the keyboard-shortcut overlay was rebuilt with live search, the update check moved to a server-side proxy, the update notification became a corner toast, a proper favicon set shipped, and the force_https setting can no longer lock you out.

The feature set from 1.0.1-beta is unchanged. No breaking changes. If you're on 1.0.1-beta, this is a drop-in upgrade.

🔒 Security

X-Forwarded-For is no longer trusted by default

getClientIp() consulted X-Forwarded-For and X-Real-IP before REMOTE_ADDR. On a direct deployment those headers are fully attacker-controlled, so the value Ledger used as "the client IP" could be spoofed — which defeated three things at once:

  • IP allowlist — send X-Forwarded-For: <an-allowlisted-ip> and walk straight through it.
  • Brute-force lockout — rotate the header each request and the per-IP counter never trips, so login attempts are effectively unlimited. (Spoofing a victim's IP could also lock the victim out.)
  • Audit log — the recorded source IP was whatever the attacker chose.

Forwarded headers are now honored only when the request originates from an IP in the new trusted_proxies config list, and the left-most entry is validated with filter_var(..., FILTER_VALIDATE_IP). The list is empty by default, which means forwarded headers are ignored entirely. Configure it under Settings → Trusted Proxies if Ledger sits behind a reverse proxy.

Read-only mode hardened

Three gaps closed:

  • Stacked queries are blocked at the driver (MYSQL_ATTR_MULTI_STATEMENTS=false), so EXPLAIN SELECT 1; DROP TABLE x can't smuggle a write through the explain endpoint.
  • The explain_query AJAX action gained the same read-only guard every other write path already had — it previously accepted raw SQL with no check.
  • isWriteQuery() now strips leading SQL comments before keyword-matching and recognizes writing CTEs (WITH … DELETE …). Both /* */ DELETE … and WITH x AS (…) DELETE … previously slipped past as non-writes.

The keyword heuristic is still a heuristic; for a hard boundary, run Ledger with a grant-limited read-only MySQL user.

Stored XSS in the Processes view

Every field in the process-list row was HTML-escaped except the database name, which can legally contain markup (CREATE DATABASE accepts almost anything in backticks). A database named with an onerror payload would execute when an admin opened the Processes tab. Now escaped like the rest. The shared escapeHtml helper in ledger.js was also aligned with the other two copies to escape single quotes.

Other hardening

  • DSN parameter injection guarded. The db request parameter is rejected if it contains ; or control characters before being interpolated into the PDO DSN.
  • TOTP codes can't be replayed. A valid 2FA code was accepted for its full ±1 step (~90 s) window with no record of use. The matched time-step is now burned per-user, so a captured code can't be reused within its window.
  • Lockout state moved out of system temp. Failed-attempt tracking lived in sys_get_temp_dir(), world-readable on shared hosts. It now lives in the web-protected logs/ directory.
  • Legacy plaintext credentials auto-upgrade to bcrypt. A hand-edited plaintext password in config.php still logs in once, then is silently rewritten as a bcrypt hash so it never persists as plaintext.

🛠 Fixed

Saved Queries no longer fatals

A dropped function ledger_saved_queries_file(): string signature left the function undefined while the rest of the file still parsed. Every saved-query action (save, list, update, delete) returned a 500 / malformed JSON — the whole feature was dead on arrival. Restored.

force_https can no longer lock you out

Enabling "Force HTTPS redirect" on a server with no working HTTPS listener (e.g. default XAMPP, no 443) previously 301'd every request to an unreachable https:// URL. Because the redirect was a permanent 301, browsers cached it indefinitely — so even after fixing config.php the browser kept auto-redirecting out of its own cache until cleared.

Three fixes: the redirect is now a 302; the setting can only be enabled from an already-HTTPS session (proving HTTPS works before committing to it); and the Settings checkbox is disabled over plain HTTP with an inline explanation, so you learn about the constraint before clicking Save.

Latent ReferenceError in the update banner

A fallback path in the old banner-rendering code referenced an undefined repo variable. Never triggered in practice (the upstream always supplied a URL) but would have crashed if it hadn't.

✨ New Features

Keyboard-shortcut overlay rebuilt

The ? overlay was a full rewrite:

  • Live search at the top — matches key labels, descriptions, and section names (searching sql pulls in the entire SQL Editor section), autofocused on open.
  • Single-column layout so descriptions get the full row width and no longer wrap mid-phrase.
  • Platform-aware keys — ⌘ ⇧ ⌥ ⏎ on Mac (detected via navigator.platform), with a hint line saying which set is shown.
  • Command-palette Esc behavior — first Esc clears the search, second closes the overlay.
  • Empty state when search returns nothing, and a mobile-stacked layout under 600px.

Favicon set

Previously no favicon link existed in any head section, so browsers showed the default globe. Ships the three-bar ledger mark on a rounded-square green background:

  • Light (#16a34a background, white bars) and dark (#4ade80 background, dark bars) variants, selected via prefers-color-scheme.
  • 32px PNG fallback for older browsers, 180px apple-touch-icon for iOS home-screen pins.
  • Wired into all four head sections: main UI, installer, login, and 2FA verification.

🎨 Changed

Update check runs through a server-side proxy

The browser previously fetched tryledger.dev/api/version.json directly via CORS, which failed silently whenever a CDN stripped the CORS headers — and exposed every admin's IP to the endpoint. The browser now calls a same-origin ajax.php?action=check_update, handled by a new UpdateCheck class that:

  • Caches the upstream response in logs/version-cache.json for 24 hours.
  • Fetches server-side via cURL (with a file_get_contents fallback) when stale, sending no cookies and no referer.
  • Serves stale cache marked stale:true if the upstream is down, so the banner survives an outage.
  • Runs before the DB connection, so the notification still works when MySQL is down.

Net effect: one outbound request per install per day instead of one per browser per day, and the same-origin fetch can't be blocked by CORS or stripped by a CDN.

Update notification is now a toast

The full-width banner that pushed page content down was replaced with a small top-right toast — slide-in animation, pulse dot, theme-driven colors across all 20 themes. Security updates get a distinct amber treatment and omit the dismiss button (security updates demand action, not dismissal). Respects prefers-reduced-motion.

📦 Deployment

  • nginx config shipped. The repo only carried an .htaccess, so on nginx none of the file protections applied — GET /logs/queries.log would serve the query history as plaintext. Added nginx.conf.example mirroring the Apache rules, plus a deny-all logs/.htaccess as belt-and-suspenders.
  • .gitattributes added pinning all text files to LF, so Windows checkouts stop producing phantom LF↔CRLF churn in diffs.

⚙️ Internal

  • Auth::getClientIp() — defaults to REMOTE_ADDR; only reads forwarded headers from a configured trusted_proxies IP, taking and validating the left-most entry.
  • Auth::isWriteQuery() — strips leading comments and detects writing CTEs before the keyword match.
  • LedgerTOTP::verifyAt() — new method returning the matched time-step, used by Auth to burn used steps and reject replays.
  • Database::connect() — sets MYSQL_ATTR_MULTI_STATEMENTS=false and validates the db name against DSN injection.
  • UpdateCheck — new class backing the server-side update proxy with disk caching and a stale-cache fallback.

⚠️ Known Issues

  • The read-only keyword heuristic isn't a hard boundary. It now resists the known bypasses, but classifying SQL by string-matching in PHP will always be best-effort. For an enforced boundary, connect Ledger with a read-only MySQL account.
  • Very large imports are still slow even with fast mode on. Carried over from 1.0.1-beta — a 1.6M-statement dump projects ~10–15 minutes on a typical server. The limit is PHP↔MySQL round-trip latency; use the mysql CLI (mysql < dump.sql) for multi-gigabyte dumps.
  • upload_max_filesize and post_max_size in php.ini still cap what PHP will accept. Raise these for larger uploads, or use the CLI.

🚀 Install

cd /path/to/htdocs
git clone https://github.com/ClearanceClarence/Ledger.git ledger

Open http://localhost/ledger/ and follow the 3-step installer.

Or download the Source code (zip) below, extract it into your web root, and open it in a browser.

For Docker, see [DOCKER.md](https://github.com/ClearanceClarence/Ledger/blob/main/DOCKER.md).

🔄 Upgrading from 1.0.1-beta

No breaking chan...

Read more

Release v1.0.1-beta

Choose a tag to compare

@ClearanceClarence ClearanceClarence released this 19 May 19:16

Ledger 1.0.1-beta is the first hardening release after the public 1.0.0-beta launch. First contact with real-world usage uncovered three real problems: the installer was too forgiving of bad input, post-login redirects didn't preserve the user's destination, and exports/imports broke on databases larger than the default memory_limit. This release fixes all three.

Also brings a quality-of-life redesign of the header info chips and an honest Fast mode toggle for the SQL import that wraps it in transactions for a 5–20× speedup on large dumps.

The feature set from 1.0.0-beta is unchanged. No breaking changes. If you're on 1.0.0-beta, this is a drop-in upgrade.

✨ New Features

Live progress UI for SQL imports

Replaces the previous "form submit, wait two minutes, hope for the best" experience. The new flow streams real progress through four phases:

  1. Upload — real bytes-uploaded / total via XMLHttpRequest.upload.onprogress
  2. Counting — quick pre-scan to give the bar a real denominator
  3. Importing — X / Y statements · N errors · MM:SS, updated ~10 times per second
  4. Done — inline result card, no page reload

NDJSON streaming (one JSON object per line) on the response side, rate-limited to ~10 updates/sec to avoid saturating the network on tiny dumps. Falls back to the original synchronous form submit if JavaScript or FormData isn't available.

Fast import mode

New toggle on the SQL import form, default on. Wraps DML statements in transactions and disables foreign_key_checks / unique_checks for the duration of the import, committing at each DDL boundary.

  • Typical speedup is 5–20× on INSERT-heavy dumps because per-statement fsyncs become per-transaction fsyncs.
  • On error, the current transaction rolls back and the import aborts. Tables committed at earlier CREATE / ALTER / DROP boundaries remain. The result card explains this honestly so users know which tables are in place after an abort.
  • Advanced users can turn the toggle off for per-statement isolation.

Multi-row INSERT exports

Exports now produce INSERT INTO t (cols) VALUES (...),(...),(...); form instead of one INSERT per row. Batched every 500 rows or ~4 MB (whichever first, staying safely under max_allowed_packet).

  • Cuts exported statement count and file size by ~500×.
  • Resulting dumps import dramatically faster, especially with Fast mode on.

Password generator and strength meter in the installer

The admin-account step on step 2 of the installer now has:

  • Generate button — produces a 20-character password via crypto.getRandomValues() from a 78-char alphabet that avoids ambiguous chars (0/O, 1/l/I) and shell-unfriendly chars (\ ' ` $). Fills both password fields, auto-reveals them, and copies to clipboard.
  • Live strength meter — 5-segment bar (red → green) with a 4-rule visible checklist (length, max length, not a common password, not equal to username).
  • Reveal-eye toggle on both password fields.
  • Submit button is disabled until the password is strong enough and matches the confirmation.

🛠 Fixed

Post-login redirect now preserves the destination URL

Visiting /?db=astrahedron&tab=sql while logged out previously redirected to the login screen correctly, but successful login landed the user on / instead of their intended URL. The cause was an interaction with the session cookie's SameSite=Strict flag that sometimes prevented the cookie from being delivered on the capturing GET request.

Fix: pass the return URL through both the session AND a hidden form field, prefer POST over session. Open-redirect protection unchanged — rejects full URLs, protocol-relative URLs, absolute paths, scheme markers, control characters, auth-flow actions, and anything over 2048 chars.

Installer is now more forgiving (and stricter)

  • Database connection errors are actionable. Instead of Connection failed: SQLSTATE[HY000] [1045] Access denied for user 'rooting'@'localhost' (using password: NO), the installer translates common SQLSTATE codes into plain English with specific hints — wrong password vs empty password, DNS failure vs connection refused vs timeout, SSL handshake issues, bind-address restrictions.
  • Step navigation is guarded. Direct GET access to ?step=2 or ?step=3 (back button, manual URL editing, stale bookmark) reliably falls back to step 1 instead of rendering a blank form. Only POST submissions can advance.
  • Password rules are stricter and visible. Minimum 8 characters (was 6), maximum 72 characters (bcrypt's truncation limit — was unlimited, so users could think they had a 256-char password when bcrypt only saw the first 72), banned against a ~100-entry list of most-leaked passwords, username can't equal password. All enforced server-side; the client-side meter is advisory only.

SQL exports stream instead of buffering

All four export routes (export_sql, export_csv, single-statement export_db, phpMyAdmin-compatible export_db) now stream rows directly to output via an unbuffered PDO cursor. Memory stays constant regardless of table size. Previously a ~300 MB database would exhaust memory_limit before any output was sent.

SQL imports stream from disk

The uploaded file is read in 64 KB chunks via fopen / fread instead of file_get_contents(), and the statement parser is now resumable across chunk boundaries (handles partial strings, comments, and DELIMITER directives at chunk edges). The result array is capped at 50 success + 100 error entries with a truncated flag; total counts remain accurate.

For a 12 MB / 100K-statement test dump, peak memory dropped from ~30 MB to ~145 KB.

🎨 Changed

Header info chips redesigned

Pill shape replaces rounded rectangles. Each chip type carries a --chip-accent CSS variable that colors only its icon:

  • Server chip (accent green) with a soft pulsing dot to telegraph "connection healthy"
  • Database version (info blue)
  • Database count (purple)
  • Uptime (warning amber)
  • PHP version (neutral, dimmed)

Uses theme variables throughout, so all 20 themes pick up the styling automatically via the base+overlay theme architecture. Respects prefers-reduced-motion.

⚙️ Internal

  • Database::splitSqlStatementsStreaming() — refactored variant of the existing tokenizer that returns [completeStatements, remainder, currentDelimiter]. When the parser encounters an unterminated statement, quoted string, comment, or DELIMITER directive at the end of its input, it stops and returns the unconsumed portion as the remainder.
  • Database::executeSqlDumpFromFile() — streams the file from disk, executes each statement as it's parsed. Takes an optional progress callback and an optional fast flag.
  • Database::countStatementsInFile() — fast pre-scan that runs the streaming parser in count-only mode.
  • Database::streamTableData() — unbuffered cursor + multi-row INSERT batching. Replaces the old fetchAll-based exportTable() for HTTP export paths.
  • Database::streamTableCsv() — companion CSV variant.
  • ajax.php action import_stream — streams NDJSON progress events. Drains output buffers, sends X-Accel-Buffering: no for nginx, calls set_time_limit(0).

⚠️ Known Issues

  • Very large imports are still slow even with fast mode on. A 1.6M-statement dump projects ~10–15 minutes of run-time on a typical server even after the transactional batching wins. The fundamental limit is round-trip latency between PHP and MySQL. Users importing multi-gigabyte dumps should use the mysql CLI directly (mysql < dump.sql), which is significantly faster than anything PHP can achieve.
  • upload_max_filesize and post_max_size in php.ini still cap what PHP will accept regardless of any code-level improvements. The default is often 8–100 MB on shared hosts. Raise these in php.ini for larger uploads, or use the CLI for very large imports.

🚀 Install

cd /path/to/htdocs
git clone https://github.com/ClearanceClarence/Ledger.git ledger

Open http://localhost/ledger/ and follow the 3-step installer.

Or download the Source code (zip) below, extract it into your web root, and open it in a browser.

For Docker, see [DOCKER.md](https://github.com/ClearanceClarence/Ledger/blob/main/DOCKER.md).

🔄 Upgrading from 1.0.0-beta

No breaking changes. To upgrade:

cd /path/to/ledger
git pull

Or for zip-based installs: extract the new release over your existing files (keep config.php and the logs/ directory).

No database migrations. No config changes required. Existing sessions, saved queries, ER diagram layouts, and query history are preserved.

If you have Check for updates enabled (default), 1.0.0-beta installs will see a banner pointing at this release within 24 hours.


Pure PHP + vanilla JS. Zero dependencies. Drop-in install.

Full changelog: [CHANGELOG.md](https://github.com/ClearanceClarence/Ledger/blob/main/CHANGELOG.md)

Release v1.0.0-beta — first public release

Choose a tag to compare

@ClearanceClarence ClearanceClarence released this 15 May 19:01

Ledger is now publicly available. Previously developed privately as DBForge between December 2025 and May 2026, now rebranded and released for general use.

The feature set is complete and tested in active development, but this is the first version validated outside of internal use. Expect rough edges on PHP versions, MariaDB forks, and hosting environments other than the development setup. Please file issues for anything that breaks.

🎉 Rebrand: DBForge → Ledger

  • New name and identity. 428 references swept across the codebase in three case forms.
  • New logo. Brackets framing three horizontal bars — a book/ledger metaphor. Available as a square mark and full wordmark.
  • JavaScript globals. DBForge.* is now Ledger.*.
  • PHP constant. DBFORGE_VERSION is now LEDGER_VERSION.
  • Install path. Moves to the repo root after the directory flatten (see below).
  • Storage keys. Cookies and localStorage formerly prefixed dbforge_ are now ledger_. Existing users log in once and re-pick their theme.

✨ New Features

Multi-statement SQL execution

Paste a full migration with multiple CREATE TABLE, INSERT, and ALTER statements separated by semicolons. Each statement runs in sequence with its own result card showing row counts, errors, and timing. Execution stops on the first failure — use START TRANSACTION / COMMIT for atomicity.

  • DELIMITER directives parse correctly, so stored procedure definitions with BEGIN ... END// bodies work as expected.
  • USE works mid-script. Paste USE mydb; CREATE TABLE foo (...); INSERT INTO foo ... and the second and third statements run against the right database. Previously each statement reconnected and lost the USE context.
  • Bootstrapping scripts work end-to-end. CREATE DATABASE new; USE new; CREATE TABLE t (...); runs cleanly because the connection no longer fails on an unknown-database error.

Index management

The Indexes panel on the Structure tab got a complete overhaul.

  • Composite indexes display on a single row with their columns comma-separated.
  • Kind badges: PRIMARY (gold), UNIQUE (blue), INDEX (purple), FULLTEXT and SPATIAL (purple with type label).
  • Add Index form with type dropdown (PRIMARY / UNIQUE / INDEX / FULLTEXT / SPATIAL), optional name, and a click-to-select column picker with numbered order badges.
  • Per-index Drop button with confirmation. PRIMARY KEY drops get extra-cautious wording.

phpMyAdmin-compatible SQL exports

The database SQL export got a new Style selector below the mode radio.

  • Single-statement — existing behavior, everything inline in one CREATE TABLE.
  • phpMyAdmin-compatible — 4-pass output: tables → data → indexes → constraints. Byte-for-byte interchangeable with phpMyAdmin's own dumps. Imports cleanly on servers where FOREIGN_KEY_CHECKS can't be disabled.

ER diagram improvements

  • Reset button (red, with confirmation) deletes the saved layout for the current database and rebuilds from auto-layout against the live schema.
  • Stale-schema bug fixed. AJAX responses were being cached indefinitely, so adding or removing tables didn't reflect in the diagram without a hard refresh. Added global no-cache headers to all 70+ AJAX endpoints.

Update notifications

Logged-in users see a banner in the admin UI when a newer Ledger release is available.

  • Browser-side check — the request goes from your browser to https://tryledger.dev/api/version.json at most once every 24 hours. No server-side phone-home; the installed PHP makes no outbound calls.
  • Security releases get a distinct, non-dismissible banner so they stand out from routine updates.
  • Per-version dismissal. Dismissing a banner only suppresses that version — the next release shows up normally.
  • Opt-out under Settings → Check for updates (default on). The endpoint sends no parameters, no cookies, and only sees what a standard web access log records for any HTTP request. Full disclosure in SECURITY.md.

🔒 Security

  • Read-only enforcement is now multi-statement-aware. Previously isWriteQuery() only checked the first keyword, so a read-only user pasting USE foo; DROP TABLE bar; would have the DROP slip through. Every statement in a batch is now checked.
  • Privacy disclosure formalized. SECURITY.md now has a dedicated Privacy & network behavior section documenting exactly what data leaves your install (nothing from PHP; one optional client-side request to tryledger.dev/api/version.json if update checks are enabled).

📦 Repository Structure

The directory layout was flattened. The git repo root and the install root are now the same directory.

  • Before: clone produced a Ledger/ folder containing a ledger/ subfolder containing all the code.
  • After: clone produces a Ledger/ folder you can drop directly into your web root.
  • Docker users can now run docker-compose up from the repo root with no cd into a subdirectory.

📚 Community Files

The standard open-source community file set is now in place:

  • SECURITY.md — vulnerability disclosure policy, supported versions, in-scope/out-of-scope rules, plus the new privacy section
  • CONTRIBUTING.md — bug-report expectations, what won't be merged, code style
  • CODE_OF_CONDUCT.md — Contributor Covenant 2.1, rewritten in plain language
  • .github/ISSUE_TEMPLATE/ — structured bug report and feature request forms
  • .github/pull_request_template.md — PR checklist
  • .github/FUNDING.yml — sponsor-link scaffold (commented out)

⚠️ Migration Notes

This is the first public release, so most readers won't be upgrading from anything. For anyone who had internal DBForge installs:

  1. Install path changed. Move your install or extract this release into a fresh directory.
  2. Cookies and localStorage prefix changed from dbforge_ to ledger_. Log in once and re-pick your theme.
  3. Custom JavaScript that calls DBForge.confirm() etc. needs updating to Ledger.confirm().

Database connection settings, saved queries, ER diagram layouts, and query history are preserved (they live in logs/, which is unchanged).

🚀 Install

cd /path/to/htdocs
git clone https://github.com/ClearanceClarence/Ledger.git ledger

Open http://localhost/ledger/ and follow the 3-step installer.

Or download the Source code (zip) below, extract it into your web root, and open it in a browser.

For Docker, see DOCKER.md.


Pure PHP + vanilla JS. Zero dependencies. Drop-in install.

Full changelog: CHANGELOG.md