AlumDeck

Security tests · release 2.0.0

What we tested, and what we found.

We tested release 2.0.0 ourselves, and this page holds the whole result: every tool with its version, date and scope, every finding and what happened to it, and what the tests do not cover. These are our own tests, not an independent audit.

The tests

What was tested.

8 tests and tools, each with its version, the date it ran, what it looked at and how much it checked.

  • 577addresses scanned with OWASP ZAP
  • 151PHP packages checked for published advisories
  • 16,650files scanned for secrets
  • 2,330automated tests passed, of 2,333
The 8 tests and tools that make up this run
Test or toolVersionDateWhat was scannedCheckedIn detail
composer audit --lockedComposer version 2.10.3 2026-08-27 13:34:238 October 2026Every package in composer.lock (runtime and dev)151 packagesruntime packages: 119; dev packages: 32; advisories: 0; abandoned: 0
Vendored JavaScript vs OSV.devOSV.dev API v1/query8 October 2026alpinejs 3.17.4, @alpinejs/csp 3.17.4, leaflet 1.9.43 librariesadvisories: 0
Semgrep CEsemgrep 1.180.08 October 2026First-party code: app, routes, config, bootstrap, database, lang, resources/views, public/index.php, public/js/site.js, public/js/cookie-consent.js, public/js/pwa-install.js, public/js/extensions, public/admin-themes/shell.js; rule packs p/php, p/phpcs-security-audit, p/default, p/secrets, local:alumdeck-laravel.yml1,974 filesfiles scanned: 1,974; rules loaded: 1,080; rules for scanned languages: 342; findings: 269; errors: 0
scan-secrets.sh (--path, --history)scan-secrets.sh sha256 36da7b311362dc278 October 2026The buyer's view of the tree (git archive + vendor/) and every file name ever committed16,650 filesfiles scanned: 16,650; history commits: 363; history file names: 2,659; history hits: 0
Automated test suite (gate, one process per file)gate-suite.sh (one process per test file)8 October 2026Every test file of the commit2,333 teststests: 2,333; passed: 2,330; failed: 0; skipped: 3; files: 439; files failing: 0
OWASP ZAP (spider, passive and active scan)ZAP 2.17.08 October 2026A local copy of this release (never a live site), scanned per role: guest 78 URLs + active scan; api 166 URLs + active scan; member 212 URLs + active scan; admin 121 URLs + active scan577 URLsalert types: 16; URLs guest: 78; URLs API: 166; URLs member: 212; URLs admin: 121
Response headers of the local copy (curl)curl8 October 20266 security headers on /, /connect/login, /admin/login, /give, /events, /api/v1/openapi.json, /robots.txt7 pagesheaders checked: 42
Code reviews by AI code reviewers (Claude), directed by Cerevonix8 reviews, 25 Sep to 4 Oct 20264 October 2026Whole codebase (25 and 26 Sep); the changes up to 27 Sep; the changes up to 3 Oct (two reviews); and three reviews on 4 Oct: the changes up to 4 Oct together with our licence service, the built-in updater, and the email templates. Sign-in, sessions, authorisation, payments, uploads, SSRF, XSS, SQL, exports, the licence check, update packages and email templates.8 reviewsfindings: 110; fixed: 100; accepted: 10; false positive: 0; open: 0

The run is dated 8 October 2026 and is recorded against one version of the code, commit a3ff91d807fab5d4f9c4918bcd6e5d9267fd931e. A test that ran on another day shows its own date in the table. The automated suite: 2,333 tests in 439 files, 2,330 passed, 0 failed, 3 skipped.

The results

What the tests found.

Nothing was rated Critical. Rated High: 6 (4 fixed, 2 false positive). No finding is open.

A row in the full list can stand for several places in the code; the totals here count every place. Each finding ends in one of these states:

  • Fixed: corrected in the release named, with the test that keeps it fixed.
  • Accepted: real, and left as it is for the reason given.
  • False positive: the tool flagged something that is not a problem, for the reason given.
  • Open: real and not fixed yet, with the plan for it.
Findings by severity, counted by place
SeverityFoundFixedAcceptedFalse positiveOpen
Critical00000
High64020
Medium43231820
Low75641100
Info7081046940
All832101336980
What each test found, counted by place
Test or toolFoundBy severityFixedAcceptedFalse positiveOpen
composer audit --lockednoneCritical 0, High 0, Medium 0, Low 0, Info 00000
Vendored JavaScript vs OSV.devnoneCritical 0, High 0, Medium 0, Low 0, Info 00000
Semgrep CE269Critical 0, High 0, Medium 0, Low 4, Info 265062630
scan-secrets.sh (--path, --history)4Critical 0, High 0, Medium 0, Low 0, Info 40040
Automated test suite (gate, one process per file)noneCritical 0, High 0, Medium 0, Low 0, Info 00000
OWASP ZAP (spider, passive and active scan)443Critical 0, High 2, Medium 19, Low 1, Info 4211174250
Response headers of the local copy (curl)6Critical 0, High 0, Medium 0, Low 0, Info 60060
Code reviews by AI code reviewers (Claude), directed by Cerevonix110Critical 0, High 4, Medium 24, Low 70, Info 121001000

The full list

Every finding, and what happened to it.

89 rows, grouped by the test that reported them. Each row carries its status and the reason for it.

Semgrep CE: 12 rows, 269 places
Findings: Semgrep CE
IDSeverityWhat was foundRule or sourcePlacesStatusWhy, or where it was fixed
SG-001InfoUnescaped Blade output {!! !!}alumdeck.blade-unescaped-output139False positiveEach value is escaped first (e(), nl2br(e()), escaped str_replace parts), cleaned (clean_html(), SafeMarkdown), JSON with the HEX flags, a fixed entity or generated QR SVG, a hex-checked theme colour, or text in a plain-text email.
SG-002InfoTracking code printed as writtenalumdeck.blade-unescaped-output2AcceptedBy design: full administrators paste their own tracking code (Settings); it waits for the visitor's consent, and its script and style tags get the page's nonce.
SG-003InfoRaw SQL built from a non-literal stringalumdeck.laravel-raw-sql-non-literal37False positiveColumn and table names come from the code, never the request; every value is a bound parameter, and LIKE patterns escape their wildcards.
SG-004Infomd5() or sha1() usedphp.lang.security.weak-crypto.weak-crypto33False positiveUsed for cache keys, ETags, idempotency keys and short identifiers, never for passwords, tokens or signatures.
SG-005LowVonage inbound signature may use md5hashphp.lang.security.weak-crypto.weak-crypto1Acceptedmd5hash is one of Vonage's own signature methods; the site uses the method chosen for the Vonage account, and the HMAC-SHA methods otherwise. The comparison is constant-time.
SG-006Infounlink() on a variable pathphp.lang.security.unlink-use.unlink-use36False positiveEvery path is built by the code inside its own folders; uploaded file names are accepted only in the exact shape the upload wrote.
SG-007InfoRedirect to a non-literal addressphp.symfony.security.audit.symfony-non-literal-redirect.symfony-non-literal-redirect14False positiveFilament resource addresses, the admin path, which is validated to letters, digits, hyphens and underscores, and the signed link for downloading a backup, which the code builds itself after checking the backup; always this site.
SG-008LowSecond fetch of the site's own address without certificate checksalumdeck.http-tls-verification-off2AcceptedCheck my headers and the installer's media check fetch only APP_URL; when the verified fetch fails they try once more without checks to tell a broken certificate from no answer, and say the result is unverified.
SG-009Infoopenssl_decrypt() resultphp.lang.security.audit.openssl-decrypt-validate.openssl-decrypt-validate2False positiveSAML decryption checks the result: OpenSSL checks the GCM tag, and the CBC padding is checked in constant time before the plaintext is used.
SG-010Infoexec() in the backupphp.lang.security.exec-use.exec-use1False positiveOne helper runs the backup's tar, gzip, mysqldump and mysql commands. Each is built from fixed words and escapeshellarg() values only; the database password is passed in an options file, never on the command line.
SG-011InfoFile path from a server variablephp.lang.security.injection.tainted-filename.tainted-filename1False positiveDOCUMENT_ROOT is set by the web server, not by a visitor.
SG-012Lowunserialize() of queued notificationsphp.lang.security.unserialize-use.unserialize-use1AcceptedThe mail outbox reads back notifications it wrote itself under storage/app/mail-outbox, outside the web root and out of visitors' reach, as Laravel's own queue does.
scan-secrets.sh (--path, --history): 1 row, 4 places
Findings: scan-secrets.sh (--path, --history)
IDSeverityWhat was foundRule or sourcePlacesStatusWhy, or where it was fixed
SS-001InfoCredential-like words in third-party codecredential-content4False positiveAn option description (Laravel's down command), a PDF form constant (dompdf, twice) and a null constant (psysh): no credential.
OWASP ZAP (spider, passive and active scan): 17 rows, 443 places
Findings: OWASP ZAP (spider, passive and active scan)
IDSeverityWhat was foundRule or sourcePlacesStatusWhy, or where it was fixed
ZP-002MediumAdmin panel: the Content-Security-Policy allows eval10055 CSP: script-src unsafe-eval4AcceptedAdmin panel only: Filament 5.8.4's own templates hold 67 Alpine expressions the CSP build cannot run, and Livewire runs @script blocks with new Function(). The public site and member area run Alpine's CSP build and allow no eval; inline scripts need the page's nonce everywhere.
ZP-003MediumAdmin panel: the Content-Security-Policy allows inline styles10055 CSP: style-src unsafe-inline4AcceptedAdmin panel only: Filament writes style attributes on its components, its scripts set more, and Livewire updates bring style blocks after the page's nonce is fixed. A style cannot run code. The public site and member area allow no inline style (two pages that show an email as sent excepted).
ZP-004MediumAdmin panel: the Content-Security-Policy allows images from any https address10055 CSP: Wildcard Directive4AcceptedAdmin panel only: rich-text fields show the images their stored text holds, from any https host, and upload previews are blob: addresses. Images cannot run code. The public site allows images only from itself and the hosts each page needs.
ZP-005MediumThe REST API answers any origin (Access-Control-Allow-Origin: *)10098 Cross-Domain Misconfiguration5AcceptedThe API reads only the token in the Authorization header (AuthenticateApiToken), its routes start no session and read no cookie, and credentials are never allowed across origins, so another site cannot act with a visitor's sign-in (checked by trust_SecurityFixesTest::test_zap_the_api_ignores_a_signed_in_browser_from_another_origin). This is Laravel's default for /api.
ZP-006InfoA redirect carries a body10044 Big Redirect Detected (Potential Sensitive Information Leak)40False positiveThe body is Laravel's standard Redirecting to page, which repeats the target address; nothing else.
ZP-007InfoThe XSRF-TOKEN cookie is readable by scripts10010 Cookie No HttpOnly Flag20False positiveBy design: it is the CSRF token for the page's own scripts and is useless without the session cookie, which is HttpOnly.
ZP-008InfoA Unix timestamp in pages10096 Timestamp Disclosure - Unix20False positiveThe ?v= version of the site's own CSS and script files (their modification time).
ZP-009InfoNo X-Content-Type-Options on static files10021 X-Content-Type-Options Header Missing20False positiveOnly on files PHP's built-in server sent directly in the local copy (images, scripts, robots.txt). On Apache public/.htaccess adds nosniff to every file (docs/server has the Nginx and LiteSpeed lines); the application's own pages always carry it.
ZP-010InfoWords such as admin, select or from in scripts10027 Information Disclosure - Suspicious Comments28False positiveOrdinary words in minified JavaScript and the page's structured data, not comments that reveal anything.
ZP-011InfoA query parameter appears in an attribute10031 User Controllable HTML Element Attribute (Potential XSS)47False positiveThe page address is printed HTML-escaped (canonical and og:url), and the contact form accepts only its listed purposes.
ZP-012InfoZAP noted a sign-in form10111 Authentication Request Identified7False positiveInformational: not a finding.
ZP-013InfoZAP noted the session cookie10112 Session Management Response Identified234False positiveInformational: not a finding.
ZP-014HighPath traversal suspected (ZAP: low confidence)6 Path Traversal1False positiveZAP sent the page's own name back as the value (purpose=contact on the contact page; in an earlier scan of this release also location=alerts on the job-alert page) and the page looked the same. Both are text values: the contact purpose must be one of the listed purposes, and the job-alert location prefills a search box. Neither touches a file.
ZP-015MediumA form without a CSRF token10202 Absence of Anti-CSRF Tokens2False positiveThe form is method="dialog": it closes a pop-up in the browser and sends nothing to the server. Every form that posts carries the CSRF token.
ZP-016InfoZAP noted a script-driven page10109 Modern Web Application5False positiveInformational: not a finding.
ZP-017HighSQL injection suspected on a script file's version number (ZAP: timing only)40024 SQL Injection - SQLite (Time Based)1False positiveZAP put a database command into the version number at the end of a JavaScript file's address and timed the answer: 0.5 to 0.9 seconds, against 0.5 seconds for the unchanged address. That address is a file on disk. The web server returns the file itself and runs neither the application nor the database for it, so nothing reads the value. Asked again 160 times on a quiet machine with four different values, ZAP's two among them, the file came back in 0.3 thousandths of a second every time, the same 6,330 bytes. The slow answers during the scan came from other work running on the test machine. This row covers script and style files only: the same alert on any other address would stay untriaged.
ZX-001LowA form posted with a list where a word belongs made its page answer 500 (seen in the application log, not as a ZAP alert)application log during the member scan1FixedFixed in 2.0.0; test: trust_SecurityFixesTest::test_zap_a_list_where_a_word_belongs_never_breaks_the_form_page
Response headers of the local copy (curl): 1 row, 6 places
Findings: Response headers of the local copy (curl)
IDSeverityWhat was foundRule or sourcePlacesStatusWhy, or where it was fixed
HD-001Inforobots.txt arrived without the security headers in the local copymissing security headers on a static file6False positiverobots.txt is a static file, which PHP's built-in server sent with no headers at all. On Apache public/.htaccess adds nosniff, Referrer-Policy, X-Frame-Options and the base Content-Security-Policy to every file it serves; Permissions-Policy and Cross-Origin-Opener-Policy only matter for HTML pages, and every page of the application carries all six.
Code reviews by AI code reviewers (Claude), directed by Cerevonix: 58 rows, 110 places
Findings: Code reviews by AI code reviewers (Claude), directed by Cerevonix
IDSeverityWhat was foundRule or sourcePlacesStatusWhy, or where it was fixed
CR-001HighReview of 26 Sep: a Users-area staffer could take over accounts above them; a raised event gift kept the old paymentdocs/09 B-1, C-12FixedFixed in 2.0.0; test: i18n_SecurityReviewTest, i18n_SecurityEventGiftTest
CR-002MediumReview of 26 Sep: authenticator seed in admin pages, staff remember-me cookie, level checks, javascript: links, push SSRFdocs/09 A-1, A-2, B-2, B-3, C-2, C-36FixedFixed in 2.0.0; test: i18n_SecurityReviewTest
CR-003LowReview of 26 Sep: sign-in answer for staff-only accounts, campaign rescheduling at view leveldocs/09 A-4, B-82FixedFixed in 2.0.0; test: i18n_SecurityReviewTest
CR-004MediumReview of 27 Sep: eight Medium findings in payments, admin levels, feeds and messagingR1-01 to R1-088FixedFixed in 2.0.0; test: improve_FIX_R1*, improve_FIX_S2A_* (each names its R1 id)
CR-005LowReview of 27 Sep: 22 Low findings, and the superseded API note B-9R1-09 to R1-30, docs/09 B-923FixedFixed in 2.0.0; test: improve_FIX_R1*, improve_FIX_S2A_*, improve_FIX_EventRegistrationGuardsTest; R1-29 by the manage-level export rule (improve_E2_ExportGateScan)
CR-006MediumReview of 3 Oct: any member could give themselves door check-in rightsS3-SEC-011FixedFixed in 2.0.0; test: s3fix1_DoorApprovalTest
CR-007LowReviews of 3 Oct: eight Low findings (money labels, donor search, group rules, attachments audit, OAuth redirect and cap, API idempotency)S3-SEC-02 to S3-SEC-05, S3-SEC2-01 to S3-SEC2-048FixedFixed in 2.0.0; test: s3fix1_*, s3fix3_*, idapi_McpOAuthTest
CR-008LowOther sessions kept access to staff-only files and door check-in after a password changedocs/09 A-31FixedFixed in 2.0.0; test: trust_SecurityFixesTest::test_a3_a_password_change_ends_other_sessions_on_staff_routes
CR-009LowAnyone could force a licence check on every requestdocs/09 A-81FixedFixed in 2.0.0; test: guard_LicenceLockTest, secfix_LicenceRecheckTest (counted only on the lock screen, at most once every five minutes for the whole site)
CR-010LowAnswering going to an event ignored its membership rule and showed the online joining linkdocs/09 B-61FixedFixed in 2.0.0; test: trust_SecurityFixesTest::test_b6_an_rsvp_follows_the_membership_rule_of_the_event
CR-011LowThe emailed-code fallback of two-factor sign-in did not notify the accountdocs/09 A-51FixedFixed in 2.0.0; test: sechard_TwoFactorFallbackTest (the account is emailed and the audit log records it)
CR-012LowThe password-reset form may have answered more slowly for an address that has an account (suspected)docs/09 A-61FixedFixed in 2.0.0; test: sechard_ResetTimingTest (both reset forms answer first; the lookup and the email follow the answer)
CR-013LowEmailed links followed the request host, and any subdomain of the site's own host was accepteddocs/09 A-71FixedFixed in 2.0.0; test: sechard_EmailLinksTest, platform_SecurityTest (emailed links use the configured address; only the host, its www twin and listed names are answered)
CR-014LowA saved audience with giving conditions listed its members to staff without the see money rightdocs/09 B-41FixedFixed in 2.0.0; test: sechard_SavedAudienceMoneyTest (listed, opened, counted and offered in pickers only for staff who may see money)
CR-015LowA block was not enforced on contact requests, mentoring requests or group invitationsdocs/09 B-51FixedFixed in 2.0.0; test: sechard_BlocksTest
CR-016LowThe membership-tier gate did not cover every member page: the business directory's rule gated nothingdocs/09 B-71FixedFixed in 2.0.0; test: sechard_TierGateTest (every page a rule names exists and runs the gate)
CR-017LowForum images had a 40-megapixel limit but no memory check (denial of service on small hosts)docs/09 C-61FixedFixed in 2.0.0; test: sechard_ImagesTest (a picture that will not fit PHP's memory limit is refused before it is decoded)
CR-018LowProfile photos and archive uploads kept their EXIF data (for example a phone's location)docs/09 C-71FixedFixed in 2.0.0; test: sechard_ImagesTest (JPEG, PNG and WebP uploads are written again without their metadata before they are stored)
CR-019LowThe geocoder and AI-assistant addresses are fetched without a private-network checkdocs/09 C-4, C-52AcceptedOnly full administrators set these addresses (Settings); by design.
CR-020LowThe Content-Security-Policy allowed inline scripts and eval on every pageSEC-121FixedFixed in 2.0.0; test: csp_PolicyTest, csp_RouteRenderTest (nonces everywhere; no eval and no inline style outside the admin panel, whose remaining exceptions are ZP-002 to ZP-004)
CR-021LowLeft from CR-011: the two-factor method can still be changed with an emailed code alone, and the account is not emailed that it changeddocs/09 A-5, what the fix left1AcceptedBy design the emailed code is the way back in for staff who have lost their phone. Every change first sends a code to the account's own address, a sign-in by emailed code in place of the app now sends the security notice (CR-011), and the change is written to the audit log.
CR-022LowLeft from CR-012: under Apache mod_php or CGI the reset form's connection may still close later for an address that has an accountdocs/09 A-6, what the fix left1AcceptedThe answer is written out in full, in the same words, before the lookup starts; PHP-FPM and LiteSpeed also release the connection first. The member form is limited to 5 tries a minute and the admin form to 2. The difference was not measured on a real host.
CR-023InfoLeft from CR-013: links in notices a queue worker sends later, in the in-app bell, in push messages and inside calendar and PDF attachments still follow the request hostdocs/09 A-7, what the fix left1AcceptedThe site now answers only to its configured host, its www twin and the names the administrator lists in TRUSTED_HOSTS, so the request host is always one of the site's own names. Email sent during the request is rewritten to the configured address; a queue worker is used only if QUEUE_CONNECTION is changed from the shipped sync.
CR-024LowLeft from CR-014: a campaign sent to an audience with a giving condition showed how many people it reaches, and after sending who received it, to Communications staff without the see money rightdocs/09 B-4, what the fix left1FixedFixed in 2.0.0; test: sechard_CampaignDonorListTest (the recipient table and the audience count of such a campaign are shown only to staff who may see money)
CR-025LowLeft from CR-018: photos stored before 2.0.0, and photos staff upload in the admin panel, keep their EXIF datadocs/09 C-7, what the fix left1AcceptedThe fix covers what members upload from 2.0.0 on. An update does not rewrite files that are already stored, and pictures added in the admin panel are chosen and published by staff.
CR-026HighLicence check: on a site whose sign-in was paused, an address written with percent-encoding (for example /%61dmin) got past the pauseReview of 4 Oct, licence: F11FixedFixed in 2.0.0; test: secfix_LicencePathTest (encoded addresses, the admin panel's own update requests and staff pages are paused like the pages they reach)
CR-027MediumLicence check: a plain PHP file could stand in for built-in functions the licence code calls, so a forged licence answer would have verifiedReview of 4 Oct, licence: F21FixedFixed in 2.0.0; test: secfix_LicenceShadowTest (every call is fully qualified; a stand-in function is refused and named)
CR-028MediumLicence check: with the install record deleted, the grace period was counted from a date in the site's own database, which its owner can changeReview of 4 Oct, licence: F31FixedFixed in 2.0.0; test: secfix_LicenceAnchorTest
CR-029MediumLicence check and licence service: anyone could make a site ask for licence checks until the service throttled its key, so the site got no fresh answerReview of 4 Oct, licence: F41FixedFixed in 2.0.0; test: secfix_LicenceRecheckTest, guard_LicenceLockTest (see CR-009); licence service test-status.php (refusals no longer fill the limit, and the site holding the licence gets a signed answer every 10 minutes)
CR-030MediumLicence service: a site that had been quiet for 30 days lost its place to another site with no confirmation, which let two sites share one licenceReview of 4 Oct, licence: F51FixedFixed in 2.0.0; test: licence service test-status.php (a quiet site keeps its licence; a place is freed only by the emailed release or by us)
CR-031MediumLicence service: the standby signing key was kept on the server beside the active one, in a hosting account that also runs other sites of oursReview of 4 Oct, licence: F61AcceptedThe standby key's secret was taken off the server on 8 Oct 2026 and is now kept off it. The active key has to stay online to sign licence answers, and still shares that account. Whoever stole it could forge licence answers but, since 2.0.0, not an update: an update needs a second signature, made with a key that is never on the server (CR-039).
CR-032LowLicence check: a site whose clock was set back ignored every newer signed licence answerReview of 4 Oct, licence: F71FixedFixed in 2.0.0; test: secfix_LicenceClockTest (a signed answer to this request counts whatever the local clock says; an older answer is still refused)
CR-033LowLeft from CR-032: a site that cannot reach the licence service has only its own clock to measure the licence deadline byReview of 4 Oct, licence: F7, what the fix left1AcceptedOffline there is no other clock to trust. It takes control of the server itself, not of the application, and nothing a visitor or member can reach is affected.
CR-034LowLicence service: a site states its own address, so the service cannot prove which server is askingReview of 4 Oct, licence: F81AcceptedA self-declared address cannot be proved. A licence that checks in from two server addresses within a day is reported to us (licence service test-status.php). Nothing a visitor or member can reach is affected.
CR-035LowLicence check: staff pages outside the admin panel (door check-in, staff file downloads) stayed open while sign-in was pausedReview of 4 Oct, licence: F91FixedFixed in 2.0.0; test: secfix_LicencePathTest
CR-036LowLicence service: a buyer's country was taken from the billing address alone; the card's country was not comparedReview of 4 Oct, licence: F101FixedFixed in 2.0.0; test: licence service test-geo.php, test-webhook.php (a card from another country than the billing address holds the sale)
CR-037LowLicence service: two of its self-tests deleted real audit entries written while they ranReview of 4 Oct, licence: F111FixedFixed in 2.0.0; test: licence service test-webhook.php, test-download.php (entries a buyer wrote meanwhile are kept)
CR-038LowLicence service: our admin screen said that withdrawing a key does not affect a running site, which was no longer trueReview of 4 Oct, licence: F121FixedFixed in 2.0.0; test: licence service test-status.php
CR-039HighUpdater: an update was trusted on the licence service's online signing key alone, so whoever took over our server could have sent code to every siteReview of 4 Oct, updater: H11FixedFixed in 2.0.0; test: updfix_SecurityTest (an offer without a valid offline release signature downloads nothing); licence service test-latest.php. What is left: CR-031
CR-040MediumUpdater: a visitor's request arriving as an install finished could undo a good updateReview of 4 Oct, updater: M11FixedFixed in 2.0.0; test: updfix_GuardTest (the guard takes the lock before it reads the plan)
CR-041MediumUpdater: an update replaced the whole .htaccess, dropping rules the site's owner or cPanel had added (password protection, IP blocks)Review of 4 Oct, updater: M21FixedFixed in 2.0.0; test: updfix_HtaccessTest (only the product's own marked block is replaced)
CR-042LowUpdater: the install step trusted its plan file and the staged files as found on diskReview of 4 Oct, updater: L11FixedFixed in 2.0.0; test: updfix_SecurityTest (the plan is signed, every path is checked again and staged files are hashed again at install)
CR-043LowUpdater: "already the same" was decided by size and CRC-32, so a file altered to match both would have been keptReview of 4 Oct, updater: L21FixedFixed in 2.0.0; test: updfix_SecurityTest (files are compared by SHA-256)
CR-044LowUpdater: scheduled tasks kept running on half-updated files during an updateReview of 4 Oct, updater: L31FixedFixed in 2.0.0; test: updfix_GuardTest (scheduled commands are skipped during an update, and undo one that was cut off)
CR-045LowUpdater: the download was not capped at the signed size, and a redirect could fall back to httpReview of 4 Oct, updater: L41FixedFixed in 2.0.0; test: updfix_SecurityTest
CR-046LowUpdater: where the host forbids clearing PHP's opcode cache, the wait for it was capped at 10 secondsReview of 4 Oct, updater: L51FixedFixed in 2.0.0; test: updfix_SecurityTest (the update is refused when the cache can be neither cleared nor waited for)
CR-047LowUpdater: the list of files an update must never replace was case-sensitive and did not allow for Windows file namesReview of 4 Oct, updater: L61FixedFixed in 2.0.0; test: updfix_SecurityTest (names are judged in lower case and against a list of allowed folders)
CR-048InfoUpdater: seven smaller points (a warning on a malformed token, the holding page's policy, the token cookie, cached copies of removed files, a missing guard file, a redirect loop, error text)Review of 4 Oct, updater: I1 to I77FixedFixed in 2.0.0; test: updfix_GuardTest (the first five), updfix_SecurityTest (the last two)
CR-049MediumEmail templates: staff with only the Communications area could rewrite the sign-in code, password-reset and other security emailsReview of 4 Oct, email templates: F11FixedFixed in 2.0.0; test: mailsec_AccessTest (security emails are changed by full administrators only)
CR-050MediumEmail templates: an edited email could put a reset link or sign-in code inside a link to another site, or hide it from the readerReview of 4 Oct, email templates: F21FixedFixed in 2.0.0; test: mailsec_SaveRulesTest (such a text is refused when saved, and a stored one sends the default)
CR-051LowEmail templates: the check on link addresses could be passed by starting the address with a merge tagReview of 4 Oct, email templates: F31FixedFixed in 2.0.0; test: mailsec_SaveRulesTest, mailsec_RenderRulesTest (addresses are checked again after the values are filled in)
CR-052LowEmail templates: Send a test to me had no limit, and the audit log did not name the addressReview of 4 Oct, email templates: F41FixedFixed in 2.0.0; test: mailsec_AccessTest (10 an hour for each person; the audit log names the address)
CR-053LowEmail templates: the button tag, or a block of details, placed inside an attribute broke the email's markupReview of 4 Oct, email templates: F51FixedFixed in 2.0.0; test: mailsec_SaveRulesTest, mailsec_RenderRulesTest
CR-054LowEmail templates: a translation that wrote a tag in capitals sent the tag's name in place of the value (a sign-in code, for example)Review of 4 Oct, email templates: F61FixedFixed in 2.0.0; test: mailsec_RenderRulesTest (every email in every shipped language shows its required values)
CR-055LowEmail templates: an edited email could carry an image from another site, which tells that site who opened itReview of 4 Oct, email templates: F71FixedFixed in 2.0.0; test: mailsec_SaveRulesTest, mailsec_RenderRulesTest (only the site's own images are kept)
CR-056LowEmail templates: the update that moves email wording out of Settings deleted wording it had not copiedReview of 4 Oct, email templates: F81FixedFixed in 2.0.0; test: mailsec_MigrationTest
CR-057InfoEmail templates: three smaller points (members' sign-in links and codes written to the log while email is not set up, edit-page helpers callable from the browser, a text too long for its column)Review of 4 Oct, email templates: I1 to I33FixedFixed in 2.0.0; test: mailsec_RenderRulesTest, mailsec_AccessTest, mailsec_SaveRulesTest
CR-058InfoLeft from CR-057: while email is not set up, staff sign-in codes and admin password-reset links are still written to the application logReview of 4 Oct, email templates: I1, what the fix left1AcceptedBy design: with email off, the log is the only way a full administrator can get back into the admin panel. The log is under storage/, which the install guide's layout keeps outside the web root.

No findings from: composer audit --locked; Vendored JavaScript vs OSV.dev; Automated test suite (gate, one process per file).

Open findings are described without the steps to exploit them until they are fixed. The raw tool reports, with their requests and responses, are not published; these tables are made from them.

The limits

What these tests do not cover.

Read this list as carefully as the results above it.

  • No external penetration test: every test here is in-house, run by us and by AI code reviewers (Claude) we direct. None is an independent audit.
  • ZAP's spider runs no JavaScript, so the admin panel's actions (Livewire) are covered by the automated tests and the code reviews, not by ZAP.
  • Payments were never tested with real money. Stripe was tested in its test mode on 9 Oct 2026, with Stripe's test cards: gifts once and monthly, an event ticket, a membership invoice, refunds, a declined card, an abandoned checkout, a renewal, a declined renewal and a cancellation in the product, and a purchase, a held purchase, a renewal and a cancellation in our own licence checkout. PayPal, yearly gifts and instalment charges on a saved card were tested only by the automated tests against stand-ins for the payment providers.
  • Nginx and LiteSpeed configurations (docs/server) were not tested. ZAP and the header test ran against a local copy served by PHP's own web server, so the Apache .htaccess headers were checked by reading them.
  • The encoded release build is not scanned; the same code before encoding is.
  • The built-in updater was tested by the automated tests on code that is not encoded; it has not been run end to end on an encoded build or on a live site.
  • Our licence service was reviewed in code and has its own tests; it was not scanned with ZAP.
  • Code written after the last code review (4 Oct 2026) has automated tests but has not had a code review of its own. That includes the fixes for CR-011 to CR-018 and the Back up now and Download buttons on the backup page.
  • Denial of service, social engineering, and your own host, PHP, TLS certificate and server are not tested.

On your own site

Check your own installation.

Three checks you can run yourself once AlumDeck is installed.

  1. Check the headers your host really sends

    In the admin, open Settings, Security, "Check my headers". It shows the security headers and the policy that really reach a browser on your host.

  2. Audit the packages

    On a computer with Composer, run composer audit --locked in the AlumDeck folder. composer.lock ships in the zip, so the command lists any advisory published since this release.

  3. Scan only what is yours

    If your IT team runs a web vulnerability scanner, point it only at your own installation, never at alumdeck.com or at anyone else's site.

Advisories

Advisories for released versions.

A security problem found in a release that people already run is announced here, with its fix.

No advisory has been issued for a shipped release.

AlumDeck is built on other people's software too. Before the release, the published advisories for it were checked against the exact versions that ship:

Published advisories checked for the software AlumDeck is built on
CheckDateCheckedAdvisories that apply
composer audit --locked8 October 2026151 packages0
Vendored JavaScript vs OSV.dev8 October 20263 libraries0

No advisory was reviewed and kept: the list for that, which ships with the release, is empty.

Earlier releases

The releases before this one.

This is the first release whose tests are published.

Questions

About these tests.

Has AlumDeck had an independent penetration test?

No. No independent penetration test has been done. Every test on this page is in-house: Cerevonix ran the tools itself, and the code reviews were done by AI reviewers (Claude) that Cerevonix directs.

That is why the results are published in full, beside what the tests do not cover, instead of a one-line claim.

Who did the code reviews?

AI reviewers (Claude), directed by Cerevonix. Cerevonix decides what is reviewed, and each finding is checked against the code before it is fixed or accepted.

For this release: 8 reviews, 25 Sep to 4 Oct 2026. Whole codebase (25 and 26 Sep); the changes up to 27 Sep; the changes up to 3 Oct (two reviews); and three reviews on 4 Oct: the changes up to 4 Oct together with our licence service, the built-in updater, and the email templates. Sign-in, sessions, authorisation, payments, uploads, SSRF, XSS, SQL, exports, the licence check, update packages and email templates.

Is it safe to publish findings that are still open?

Open findings are described without the steps to exploit them until they are fixed, and the raw tool reports, with their requests and responses, are not published.

A release cannot be packaged while a finding rated Critical or High is open: the packaging step reads this data file and refuses. No finding is open in this release.

Can our IT team see the raw tool reports?

The raw reports are not published. What you get is the data file this page is built from, in the zip, with a readable guide made from it.

For a questionnaire, the guide for IT reviews has a security summary and answers in the style of a HECVAT Lite, and the security review walks through one code review, finding by finding.

Is every release tested like this?

The step that builds a release zip refuses to run unless this data file is for that exact release, every finding has been triaged and explained, and no finding rated Critical or High is open. It runs the dependency audit again as it packs.

So each release carries its own results, and the data file keeps a summary of the releases before it, shown under earlier releases.

We found a problem these tests missed. How do we report it?

Through the contact form, with the topic set to "Security report". The disclosure policy says what we do next. Good-faith research is welcome; please test only an installation of your own.

The data file

Match this page with your zip.

This page is built from one data file, and so is the Security tests guide inside the zip. The SHA-256 checksum of the file this page was built from:

4428e7f9e948c9bdfa2f83f9c26e6edaef762caaafb25a73bd10fab035fb741e  docs/security-tests.json

The file ships in the AlumDeck 2.0.0 zip as docs/security-tests.json. Work out the checksum of your copy; if it matches, this page and your zip hold the same results.

Windows (PowerShell):  Get-FileHash .\docs\security-tests.json -Algorithm SHA256
macOS:                 shasum -a 256 docs/security-tests.json
Linux:                 sha256sum docs/security-tests.json

Open the live demo. It puts itself back every night.

Sign in as an admin or a member and click anything. Nothing you do can break it.

Sign-in details on the demo pageResets every night

Type a word, or start with one of these pages.

↑ ↓ moveEnter openEsc close