|
| 1 | +--- |
| 2 | +title: 'Wheels 4.0.5: a hardening release — 100+ fixes across security, performance, and deploy, now installable anywhere' |
| 3 | +slug: wheels-4-0-5-released |
| 4 | +publishedAt: '2026-06-19T13:30:00.000Z' |
| 5 | +updatedAt: '2026-06-19T13:30:00.000Z' |
| 6 | +author: Peter Amiri |
| 7 | +tags: |
| 8 | + - wheels-4 |
| 9 | + - release-notes |
| 10 | + - frameworks |
| 11 | +categories: |
| 12 | + - Releases |
| 13 | +excerpt: >- |
| 14 | + Wheels 4.0.5 is out, and together with 4.0.4 it's one of the most substantial |
| 15 | + hardening passes on the 4.0 line — 100+ changes spanning security (open-redirect |
| 16 | + and info-disclosure fixes, fail-closed gates, SQL-surface tightening), warm-path |
| 17 | + performance, a much tougher `wheels deploy`, and Adobe CF / BoxLang cross-engine |
| 18 | + fixes. 4.0.5 then makes the whole thing installable the same way on every major |
| 19 | + platform — Homebrew, Scoop, apt, dnf — including arm64 Linux, verified daily. |
| 20 | +--- |
| 21 | + |
| 22 | +Wheels 4.0.5 is out — and the story is really two releases. **4.0.4 was a large hardening pass (100+ commits across security, performance, and deploy), and 4.0.5 makes that work installable the same way on every major platform, including arm64 Linux.** 4.0.4 went out and was superseded by 4.0.5 the same day, before any announcement, so this post covers both. |
| 23 | + |
| 24 | +## Security hardening |
| 25 | + |
| 26 | +4.0.4 closed a batch of real security gaps in the framework's request surface: |
| 27 | + |
| 28 | +- **Open-redirect hardening.** `$isSafeRedirectUrl()` now normalizes input per WHATWG URL parsing (stripping embedded tab/CR/LF) before it classifies a redirect, and rejects backslash and schemeless-authority tricks (`/\evil.com`, `\/evil.com`, `https:/evil.com`). |
| 29 | +- **Information disclosure.** The HTML and JSON branches of `/wheels/info` no longer render secrets — `csrfCookieEncryptionSecretKey` is no longer printed in plaintext, and the JSON branch no longer dumps the full application metadata (datasource credentials, ORM settings). |
| 30 | +- **Fail-closed by default.** The production block on `/wheels/*` dev-UI handlers is now a fail-closed allowlist, and the URL reload gate refuses to act unless a non-empty `reloadPassword` is configured — `?reload=true` with no password set is denied, not allowed. |
| 31 | +- **Path traversal.** `$getRequestFormat` rejects non-alphanumeric `url.format` values, closing a local-file-inclusion vector through the error-template include. |
| 32 | +- **SQL surface.** Scope-handler arguments and `findAll(select=)` items are tightened (values like `Union Pacific` round-trip unchanged while genuinely suspicious `select` items get flagged), and `updateAll(include=)` join parsing no longer splits on a bare `ON` token inside an identifier. |
| 33 | +- **Proxy trust.** A new `trustProxyHeaders` setting (default `false`) governs whether `X-Forwarded-*` is believed — `isSecure()`, maintenance-mode IP exceptions, and the reload rate-limit key all stop trusting client-supplied forwarded headers unless you opt in. |
| 34 | +- **Deploy secrets** are redacted from remote-execution failure messages and sent over SSH stdin (`docker login --password-stdin`) so they never surface in output. |
| 35 | + |
| 36 | +## Performance |
| 37 | + |
| 38 | +Ten warm-path optimizations, focused on the work every request repeats: |
| 39 | + |
| 40 | +- `model()` and `controller()` take a **lock-free fast path** on cache hits instead of a reflective double-checked lock. |
| 41 | +- `URLFor()` route lookups are memoized in application scope (with negative caching), and database column metadata is cached per datasource+table. |
| 42 | +- The per-request action-dispatch gate is now an **O(1)** lookup instead of an O(n) list scan, the HTTP status-code map and partial column lists are memoized instead of rebuilt per render, and mixin-free apps no longer pay for a throwaway `wheels.Plugins` instance on every request. |
| 43 | + |
| 44 | +## `wheels deploy` grew up |
| 45 | + |
| 46 | +The Kamal-port deploy command got roughly twenty fixes hardening it for real fleets: |
| 47 | + |
| 48 | +- Works on **fresh hosts** now (the proxy boot guard never reached `boot()`), acquires its fleet lock **all-or-nothing**, bounds secret resolution with a timeout, and writes an on-server audit trail. |
| 49 | +- Correct `rollback --destination` overlay handling, proxy traffic built from `proxy.app_port`, env-file secret delivery to app and accessory containers, stricter IPv6 host validation, and same-version redeploys that no longer collide on the container name. |
| 50 | + |
| 51 | +## Cross-engine fixes |
| 52 | + |
| 53 | +- **Adobe ColdFusion**: authorized reloads returned HTTP 500 on case-sensitive filesystems — i.e. every stock Linux Adobe deployment — and URL environment switches into production/maintenance silently reverted. Both fixed, along with a Lucee-only `cfabort;` that 500'd `GET /` on Adobe. |
| 54 | +- **BoxLang**: hardened null handling across the router, dispatcher, and error handler for BoxLang's stricter semantics. |
| 55 | +- **Oracle / SQL Server**: identity retrieval after INSERT now uses the JDBC driver's generated keys (or session-scoped sequence values) instead of the race-prone `@@IDENTITY` / `MAX(ROWID)` fallbacks. |
| 56 | + |
| 57 | +## New capabilities |
| 58 | + |
| 59 | +- **`wheels jobs work`** — a long-lived background-job worker loop (`--queue`, `--interval`, `--max-jobs`), plus `wheels jobs status`. |
| 60 | +- **`wheels upgrade apply`** swaps an app's `vendor/wheels/` for the framework bundled in the installed CLI, and **`wheels upgrade check --strict`** escalates advisory findings to a hard CI-gating failure. |
| 61 | +- A `subpath` setting for subfolder-mounted apps, a `/up` liveness/warm-up endpoint in scaffolded apps (used by the deploy healthcheck), and per-tool MCP input schemas. |
| 62 | + |
| 63 | +…and roughly **eighty fixes** in total — routing `scope()`/`namespace()` callbacks, named capture groups, CORS duplicate-header arbitration, multi-node `RateLimiter` storage, honest `migrate`/`seed`/`test` exit codes and output, `hasMany` shortcut associations, and more. The complete list is in the [changelog](https://github.com/wheels-dev/wheels/blob/main/CHANGELOG.md). |
| 64 | + |
| 65 | +## Now install it anywhere — including arm64 |
| 66 | + |
| 67 | +4.0.5's own headline is distribution. The `wheels` CLI installs the same way on every major platform: |
| 68 | + |
| 69 | +```bash |
| 70 | +# macOS / Linux — Homebrew |
| 71 | +brew install wheels-dev/wheels/wheels |
| 72 | + |
| 73 | +# Windows — Scoop |
| 74 | +scoop bucket add wheels https://github.com/wheels-dev/scoop-wheels |
| 75 | +scoop install wheels |
| 76 | + |
| 77 | +# Debian / Ubuntu — apt |
| 78 | +curl -fsSL https://apt.wheels.dev/wheels.gpg | sudo gpg --dearmor -o /usr/share/keyrings/wheels.gpg |
| 79 | +echo "deb [signed-by=/usr/share/keyrings/wheels.gpg] https://apt.wheels.dev stable main" | sudo tee /etc/apt/sources.list.d/wheels.list |
| 80 | +sudo apt update && sudo apt install wheels |
| 81 | + |
| 82 | +# RHEL / Fedora / Rocky / Alma — dnf |
| 83 | +sudo dnf config-manager --add-repo https://yum.wheels.dev/wheels.repo |
| 84 | +sudo dnf install wheels |
| 85 | +``` |
| 86 | + |
| 87 | +The Linux `.deb`/`.rpm` are now **architecture-independent** — the CLI launches through a portable `java -jar` runner — so `apt install` / `dnf install` work on **arm64 (aarch64)** as well as x86_64: Raspberry Pi, AWS Graviton, Ampere, arm64 Linux VMs on Apple silicon. 4.0.5 also fixes a bug where the RHEL/Fedora package couldn't locate its Java runtime, so `dnf install wheels` now works out of the box on Rocky, Alma, and Fedora. Java 21 is pulled in automatically on every channel. |
| 88 | + |
| 89 | +And we now **verify all four channels every day**: a CI job installs the published CLI through Homebrew, Scoop, apt, and yum on real macOS / Windows / Ubuntu / Rocky runners (amd64 *and* arm64) and asserts `wheels --version` matches the release — so a broken `brew install wheels` becomes our problem before it becomes yours. |
| 90 | + |
| 91 | +## A note on versions |
| 92 | + |
| 93 | +If you see a **4.0.4** tag, that's expected: 4.0.4 shipped the hardening work above but its Linux packages weren't yet architecture-independent, so 4.0.5 superseded it the same day. Everything in 4.0.4 is in 4.0.5 — just install or upgrade to **4.0.5**. |
| 94 | + |
| 95 | +## Upgrading |
| 96 | + |
| 97 | +```bash |
| 98 | +brew upgrade wheels # Homebrew |
| 99 | +scoop update wheels # Scoop |
| 100 | +sudo apt update && sudo apt install --only-upgrade wheels # apt |
| 101 | +sudo dnf upgrade wheels # dnf |
| 102 | +``` |
| 103 | + |
| 104 | +Then confirm: |
| 105 | + |
| 106 | +```bash |
| 107 | +wheels --version # Wheels Version: 4.0.5 |
| 108 | +``` |
| 109 | + |
| 110 | +The framework itself lives in `vendor/wheels/` in your app — run `wheels upgrade check` to see anything worth adjusting before you bump a project, or `wheels upgrade apply` to swap it. Happy shipping. |
0 commit comments