-
-
Notifications
You must be signed in to change notification settings - Fork 27
Developer Tests Security
Part of Developer Tests. The suites that answer "is it actually refusing?" β and, just as importantly, "is it still accepting the things it should?"
π Read this before reading any result on this page. A guard that refuses everything β because of a typo in a constant, or a wrong column name β looks exactly like a guard working perfectly, if all you assert is that it refused. That is how a fail-open bug hides in a green suite. So every "it refused" assertion here is paired with a positive control showing the same code still accepts something legitimate. When one of these tests goes red, check which half failed before deciding how worried to be.
| Test | Needs |
|---|---|
security-findings/run.php |
Database (read-only); optionally a base URL |
web-exposure-guard.php |
Nothing |
record-preview.php |
Database |
record-preview-security.php |
Database (rolled back) |
sso-dangling-link.php |
Database |
ldap/01_empty_password_guard.php |
Nothing |
oidc-discovery.php |
Nothing |
directory-sync-scopes.php |
Nothing |
The nine reported security findings from August 2026 β by observable behaviour wherever it can, and by the shape of the code only where behaviour is not reachable from outside.
Almost none of these fixes have a button. There is no screen that shows "the session cookie is HttpOnly now", and no way to eyeball whether a refresh token is ciphertext at rest. This suite exists so the answer is not "trust the diff".
| Finding | |
|---|---|
| F1 | attachments named by the sender could be written into the web root |
| F2 |
setup/ handed a privilege flag to anonymous visitors |
| F3 | mailbox OAuth tokens (and five other secrets) stored in the clear |
| F5 | attachments served with a sender-chosen Content-Type, SVG inline |
| F6 | bundled TinyMCE carried four published stored-XSS CVEs |
| F7 | no session rotation, no cookie flags, no CSRF defence |
| F8 | default credentials permanent, brute-force protection shipped off |
| F9 | tenant guards failed open; two confirmed cross-company leaks |
php tests/security-findings/run.php
php tests/security-findings/run.php http://localhost/freeitsm-app/
Pass a base URL to include the live HTTP checks β most importantly the
original F2 exploit chain, which is the one worth watching fail. Without a URL
those are SKIPPED, not silently passed, so a green run with no URL has not
tested the exploit at all.
Read-only: it writes nothing to the database and nothing to the web root.
Any red here is a regression of a reported vulnerability. Identify the finding number from the label and treat it as blocking a release β these were reported from outside, so a regression is visible to the person who reported it.
That nothing in tests/ is reachable over HTTP β see
Test suite exposure for the whole story.
It checks all three layers: the PHP_SAPI guard on every script, the
.htaccess/web.config covering the whole directory, and tests/ being kept
out of the Docker image and 404'd by the shipped nginx config.
π It walks the directory rather than a list, so a test added tomorrow is checked tomorrow. Nobody has to remember to register it.
php tests/web-exposure-guard.php
12 assertions, needs nothing.
"every .php in tests/ refuses to run" names the files missing their guard.
Add this as the first thing after <?php:
if (PHP_SAPI !== 'cli') { http_response_code(404); exit; }Any other failure means one of the outer layers has been removed β put it back rather than relying on the remaining two.
At-a-glance record previews. A preview is a read, and the links that lead to one can point at records the reader may not open β so the refusals matter more than the happy path, and each has a positive control beside it.
php tests/record-preview.php
ZZPV-named rows, removed including on failure.
A refusal going red means a preview is rendering a record the reader has no route to. The preview is a summary, so the leak is small but real β names, titles, statuses.
The security boundary around previews, written after the direct question "is there a way a hacker could call the preview function unauthenticated?" β as checks rather than as a claim.
π΄ The interesting question is never "does it work" but "what does it refuse, and does the refusal say anything it should not" β a refusal that distinguishes "no such record" from "not yours" is itself a disclosure.
analyst_tenant_access is empty, meaning nobody is
limited, and the test would otherwise prove nothing. It runs inside a transaction
that is always rolled back β nothing here is ever committed.
php tests/record-preview-security.php
Distinguish the two kinds: a preview returned when it should not (a leak), or a refusal that differs depending on why (an oracle). The second is subtler and still worth fixing.
A sign-in link that outlived its account. Reported after importing the Core demo data: an OIDC account vanished from the admin interface and became impossible to sign back into, permanently, with "Your account is no longer available."
The mechanism, which is what the test pins down:
-
api/system/import_demo_data.phpemptiesanalysts(baradmin) anduserswithFOREIGN_KEY_CHECKSoff, so theON DELETE CASCADEon the two identity tables never fires and the links are left dangling. -
oidc_callback.phpresolves a person by(provider, subject)first. A dangling link wins that lookup, the account load returns null, and the sign-in dead-ends β because email matching and just-in-time provisioning both live in the branch past it. - So re-creating the account by hand does not help either. The link still points at the old id.
The irony worth keeping: had the cascade fired, nobody would have noticed. The link would have gone with the account, the next sign-in would have landed in the JIT branch, and the account would have quietly come back.
php tests/sso-dangling-link.php
ZZTEST-prefixed rows, removed including on failure.
Check whether a new bulk-delete path has been added that disables foreign key checks. That is the root cause, and it will produce this symptom again for a different table.
The RFC 4513 unauthenticated bind guard. LDAP defines a bind with a DN and an empty password as an "unauthenticated bind", and many directories answer it with success. Without an explicit check, leaving the password box blank would log you in as anyone whose username you can guess.
So instead the test points the provider at an unroutable address (TEST-NET-1,
RFC 5737) with a short timeout. If the guard runs, ldapAuthenticate() returns
immediately with reason credentials and never opens a socket. If the guard is
removed, the call instead spends the connect timeout and comes back with a
config/network error.
π Speed is the assertion: no network call can have happened.
php tests/ldap/01_empty_password_guard.php
14 assertions, no directory required.
If it now takes seconds rather than returning instantly, the guard has been removed or moved below the connection attempt. Put it back before any socket is opened.
Validation of a provider's discovery document. The assertion that matters is not "a good document is accepted" β it is that a bad one is refused here, where we still know whose fault it is.
A provider publishing authorization_endpoint as a bare path (/oidc/authorize
rather than https://idp.example.com/oidc/authorize) used to be passed straight
through. oidcBuildAuthUrl() then produced a relative Location: header, the
browser resolved it against the current origin β this application β and the user
landed on our host with a 404. Every visible symptom pointed at FreeITSM. That is
exactly how it was reported.
Mostly negative cases, with positive controls proving the guard is not simply
refusing everything. No database, no network: oidcIsAbsoluteHttpUrl() is a pure
function, which is precisely why it is the thing worth testing.
php tests/oidc-discovery.php
27 assertions.
A bad document being accepted means the next misconfigured provider produces a support request that looks like our bug. Fix the validator, and make sure the error names the provider's field.
Which parts of a directory are in scope β the arithmetic that decides who gets imported. Pure logic, no fixture and no database, so it can be checked on any machine without starting a directory.
Two rules earn most of the cases:
-
A ticked branch means the whole branch, so being "under" something is
decided by DN suffix β and the comma in that suffix is load-bearing.
Without it
OU=SalesmatchesOU=WholesaleSales, and a carve-out silently swallows an unrelated department. - An install upgraded from before the OU browser has neither column set, and must go on importing exactly who it imported yesterday. The fallback is not a nicety: without it, upgrading imports nobody, and the sanity brake is then the only thing standing between that and every person in the company being marked as having left.
php tests/directory-sync-scopes.php
19 assertions, needs nothing.
The suffix rule is the one to check first, and the upgrade fallback is the one with the worst blast radius β it marks an entire company as leavers.
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π REST API: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ π’ One board across every company
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)
- What this is
-
π Bugs resolved
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96