Upgrade to Laravel 13, bump to v2.8.0, and remediate vulnerabilities#1574
Merged
Conversation
- laravel/framework ^11.45 -> ^12.0 (installed 12.63.0) - phpunit/phpunit ^10.5 -> ^11.0; migrate phpunit.xml to the 11.5 schema - graham-campbell/github ^12.5 -> ^13.0 (v12 caps illuminate/support at ^11) - Carbon 3 pulled in by L12; no application code changes required - gitignore /.phpunit.cache Full suite green: 59 tests, 128 assertions (1 skipped).
- laravel/framework ^12.0 -> ^13.0 (installed 13.19.0) - laravel/tinker ^2.9 -> ^3.0 (installed 3.0.2, pulls psysh >=0.12.19) - phpunit/phpunit ^11.0 -> ^12.0 (installed 12.5.31) - Symfony components move to 8.x (supported by L13); symfony/yaml stays on patched 7.4.14 via its direct ^7.0 constraint - league/commonmark auto-bumped to 2.8.2 (patched) - No application/config code changes required; the fluent validateCsrfTokens(except: ...) config still resolves under L13 composer audit: No security vulnerability advisories found. Full suite green: 59 tests, 128 assertions (1 skipped).
Add a package.json "overrides" block forcing patched versions of vulnerable transitive build dependencies (shell-quote, ws, node-forge, serialize-javascript, lodash, minimatch, path-to-regexp, svgo, postcss, qs, uuid and others). This takes `npm audit` from 41 vulnerabilities (1 critical, 13 high, 18 moderate, 9 low) down to 9 (0 critical, 0 high, 3 moderate, 6 low). The 9 residuals are all dev/build-time-only advisories in the EOL laravel-mix@6 toolchain (the elliptic crypto-polyfill chain with no upstream patch, ajv 6.x under babel-loader, webpack-dev-server / laravel-mix, and webpack) that cannot be cleared without replacing laravel-mix; none ship in Heimdall's production runtime. Both 'npm run production' and 'npm run dev' still compile successfully.
Add tests guarding the surfaces most likely to break on a future
Laravel/PHP upgrade:
- Helper globals: format_bytes, parse_size, className, get_brightness,
title_color (tests/Unit/helpers)
- CSRF exception config actually reaches the framework
(PreventRequestForgery neverVerify list) and the excepted routes resolve
- Core GET routes boot and render on the current framework
- Filesystem disks resolve and the local disk root stays pinned to
storage_path('app') (guards the Laravel 12 default-root change)
ItemController::appload() was declared ': ?string', so its two error branches that 'return response()->json([...], 404)' had the JsonResponse coerced through Response::__toString() into a raw HTTP message served as an HTTP 200 body. Widen the return type to '\Illuminate\Http\JsonResponse|string|null' so those branches emit real 404 JSON responses. The method body is unchanged, so the happy path still returns the same JSON string and the frontend contract is preserved. Flip the endpoint characterization test to assert the corrected 404.
PHPUnit 12 renamed PHPT -> Phpt (src/Framework/Exception/PhptAssertionFailedError.php and the src/Runner/Phpt/ directory). Because this repo commits vendor/ and the macOS dev filesystem is case-insensitive, git kept the old-case paths in the index while Composer wrote the new case to disk, so the change went undetected locally. On CI's case-sensitive Linux filesystem the checked-out old-case files don't satisfy PHPUnit's require of the new-case names and 'php artisan test' fatals before running. Re-track these 8 files under their correct case.
- itemImport: check response.ok in fetchAppDetails so a genuine 404 (from the appload return-type fix) is reported as 'Failed to find app id' instead of being parsed as a successful import; applied to the source and the committed compiled bundle. - phpunit.xml: point the schema URL at 12.5 to match the installed PHPUnit 12.5.x. - ColorHelpersTest: exercise the get_brightness() non-hex stripping the test name promised (interior separators), which the prior assertion never covered.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Upgrades Heimdall from Laravel 11 → 13, bumps the app version to 2.8.0, and remediates the reported security vulnerabilities. Also adds upgrade-regression tests and fixes a latent bug found along the way.
Commits
laravel/framework^11 → ^12 (12.63.0), PHPUnit 11, Carbon 3,graham-campbell/github^12 → ^13;phpunit.xmlmigrated to the 11.5 schema. No application code changes.laravel/framework^12 → ^13 (13.19.0),laravel/tinker^3, PHPUnit 12; Symfony components move to 8.x. No application code changes — the fluentvalidateCsrfTokens(except: …)config still resolves (it now proxies the renamedPreventRequestForgerymiddleware).appload()404 coercion bug — see below.Security remediation
Composer — fully clean ✅
composer auditwent from 13 advisories across 7 packages (laravel/framework×3,symfony/cache·mailer·routing·yaml×7,league/commonmark×2,psy/psysh×1) to “No security vulnerability advisories found.” — resolved entirely by landing on Laravel 13.19.0 + tinker 3 + the patched Symfony / commonmark / psysh transitive versions the upgrade pulls in.npm — 41 → 9, every critical & high eliminated⚠️ (a decision for you)
npm audit: 41 (1 critical, 13 high, 18 moderate, 9 low) → 9 (0 critical, 0 high, 3 moderate, 6 low), via apackage.json"overrides"block that force-patches vulnerable transitive build deps. Nolaravel-mixchange — bothnpm run productionandnpm run devstill compile (identical output).The 9 residuals are all dev / build-time only (never in the production runtime — runtime deps are just
select2+sortablejs), and each is irreducible without replacing the EOLlaravel-mix@6toolchain:elliptic(+browserify-sign,create-ecdh,crypto-browserify,node-libs-browser)ellipticis still flagged; no upstream patch exists. Node crypto polyfills, never bundled.ajv6.xbabel-loader; forcing ajv 8 breaks the build (schema-utils 2.x uses the ajv-6 API).webpack-dev-server+laravel-mixnpm run hot, not the production build.webpackTo reach literal zero, the asset pipeline needs migrating off laravel-mix to Vite. I deliberately did not do that blind — it's an architectural change to the frontend that warrants visual QA of the dashboard. Say the word and I'll do the Vite migration as a follow-up commit on this branch.
Also included
app/Helper.phpglobals (format_bytes,parse_size,className,get_brightness,title_color), the CSRF exception config (asserts theorder/appload/test_configexceptions actually reachPreventRequestForgeryand the routes resolve), core-route boot/render, and the filesystem-disk pins (guards the Laravel 12storage/app/privatedefault-root change). Full suite: 89 tests green (1 pre-existing skip).appload()bug fix —ItemController::appload()was typed: ?string, so its tworeturn response()->json([...], 404)branches had theJsonResponsecoerced throughResponse::__toString()into a raw HTTP message served as an HTTP 200 body. Widened the return type to\Illuminate\Http\JsonResponse|string|nullso those branches emit a real 404 JSON. The method body is unchanged, so the happy path returns the identical payload and the frontend contract is preserved.Heads-up / behavior change to confirm
imagevalidation rule to reject SVGs by default.UserControlleruses'file' => 'image'for avatars, and those avatars are stored unsanitized (unlike the icon path, which runs svg-sanitize). I kept the new secure default — SVG avatars are now rejected — rather than re-open that stored-XSS vector. If you'd rather keep SVG avatars, change it to'file' => 'image:allow_svg'.Verification
php artisan --version→ Laravel Framework 13.19.0 (boots; serves HTTP 200 end-to-end through the full middleware stack)../vendor/bin/phpunit→ 89 tests, 210 assertions, 1 skipped — all green.composer audit→ no advisories.npm audit→ 9 (dev-only).npm run productionandnpm run devboth compile successfully.Environment note: the local Composer binary was an ancient 2.3.9 that can't resolve modern graphs; all dependency work used a current Composer 2.10.2. Nothing about that is committed.
CI
Green ✅ (
Run testspassing). One extra commit — “Fix committed PHPUnit vendor path casing for case-sensitive CI” — was needed: PHPUnit 12 renamedPHPT→Phpt, and because this repo commitsvendor/and macOS is case-insensitive, git kept the old-case paths while Composer wrote the new case to disk. That only fails on CI's case-sensitive Linux filesystem (PHPUnit'srequirecouldn't find the file), so it passed locally but brokephp artisan testin CI until the 8 files were re-tracked under the correct case. Worth keeping in mind for future dependency bumps done on macOS.