-
Notifications
You must be signed in to change notification settings - Fork 15
Security Hardening 2026 08
In August 2026 a security researcher, Erlend Volden, went through FreeITSM and privately reported a list of problems. We fixed them, he checked our fixes, and he found nine more things β some of them problems our own fixes had just created. This page explains that second round in ordinary language.
There is a companion page written for developers: Security hardening 2026-08 β Developer Guide. It says the same things with file names, line numbers and code.
β Status: signed off and merged. Erlend re-checked all nine against the rebased branch on 12 August 2026 and confirmed every one fixed β rating two of them better answers than the ones he had suggested. The branch is merged into
main. It is not yet in a tagged release.
β οΈ Merged is not the same as finished. The section What we deliberately did not fix yet lists real, open problems β several of them live in the current code today. Please read it rather than assuming this page describes a closed book.β‘οΈ Most of that list has since been done. All four cross-company problems, the two-factor guessing limit, the SVG logo, the setup-page disclosure and the Database Verification warning were closed in round three on 12 August 2026. What remains open after that round is listed at the bottom of the round-three page β chiefly cross-site request forgery and the SCORM content directory.
Scope of this page. It covers the second round β the nine things found when our first set of fixes was re-reviewed, plus a few small bugs we spotted alongside them. The first round is summarised on the Security page.
Every entry answers the same four questions:
- What was wrong β the mistake itself.
- How it could have been used against you β what an attacker, or bad luck, could actually have done.
- What we changed.
- Why that fixes it.
Some of these were security flaws β someone could have done something they shouldn't. Others were features we accidentally broke while fixing the security flaws. Both matter, and it's worth knowing which is which, so each one is labelled.
Type: security flaw. This is the most serious one on the page.
What was wrong. FreeITSM can host several customer companies side by side, and an analyst is normally restricted to the ones they look after. There was a check to stop someone filing a person into a company they can't reach β but the check only ran if the request said which company. If it simply didn't mention a company, the check was skipped entirely. FreeITSM then worked the company out on its own, from the email address domain.
How it could have been used against you. An analyst who only looks after Company A creates a new contact, deliberately leaving the company blank, and gives it an email address at Company B's domain β plus a password of their own choosing. FreeITSM sees @companyB.com, files the contact under Company B, and never checks whether the analyst was allowed to do that. The analyst then signs in to the customer portal as that contact and reads Company B's private knowledge articles and tickets.
The uncomfortable part is that this needed no clever trick. It needed one field left blank.
What we changed. FreeITSM now works out which company the contact will actually end up in β whether that was stated or worked out from the email address β and then checks the analyst is allowed to use that company.
Why that fixes it. The check is now applied to the real answer rather than to what the request happened to mention. Leaving the field blank no longer skips anything; it just means FreeITSM works out the answer and checks that instead.
A wrinkle worth knowing about. The obvious fix would have broken every single-company install. On those, analysts have no company assignments at all β there's only one company, so nobody bothers β and the check would have refused everything, meaning nobody could create a customer contact at all. So the check only applies once you actually have more than one company. That's also why the original flawed check existed: somebody had hit this problem before and solved it the wrong way.
Type: security flaw.
What was wrong. All the "can this analyst see this?" checks start by asking one question: does this installation have more than one company? If the answer is no, they all stand down β quite correctly, because on a single-company install there is nothing to separate.
That question was answered by a single database query. And if that query failed for any reason at all β the table briefly locked, the connection dropped, a permissions hiccup β the code treated the failure as "no, just one company". Every separation check downstream then switched itself off, and the answer was remembered for the rest of that page load.
How it could have been used against you. Mostly this is bad luck rather than an attack: a busy moment on the database, and for one page load an analyst sees another company's data. But a determined attacker who could make the database briefly struggle β even just by hammering the server β could try to make that window happen on purpose.
What we changed. Now only one specific answer means "single company": the table genuinely not existing, which is what a part-upgraded installation looks like. Every other kind of failure is treated as "assume there are several companies", so all the checks stay switched on and refuse rather than allow.
Why that fixes it. The system can no longer be talked into standing down by something going wrong. If it can't confirm it's safe to relax, it doesn't relax.
This one is the companion to a fix in the first round. We had corrected seven of these checks and missed the single question sitting in front of all of them β so the first round looked more finished than it was. That's exactly the kind of thing a second pair of eyes is for.
Type: security flaw.
What was wrong. FreeITSM ships with a starter account β username admin, password freeitsm β and it's supposed to make you choose a new password the first time you sign in. The README says exactly that.
There are two places that can create that starter account: the database file, and the Database Verification step. Only the second one set the "must change password at first sign-in" flag. And on the Docker install β the route most people use β the account is created by the database file. So the flag was never set, and admin / freeitsm stayed valid forever.
How it could have been used against you. The password is published in our own README and on our website. Anyone who finds a FreeITSM login page can simply try it. If the install went up on the internet and nobody thought to change the password β which is precisely the situation the forced change exists to prevent β that's full administrator access to everything.
What we changed. Both routes now set the flag. On top of that, Database Verification will now find an existing admin account that is still using the published password and flag it, so installations already out there get fixed too.
Why that fixes it. The forced password change now happens on every install route, not just one of them. And it's deliberately narrow: it checks whether the published password still works, rather than just looking at the username. If you changed your admin password long ago, nothing happens to you. An account that merely happens to be called "admin" with a real password is left alone.
This one bit us at home. When testing the fix, Database Verification found that your own development installation was still running on
admin/freeitsm. It's now flagged, so your next admin sign-in will ask you to pick a new password. That's the fix doing its job β and a fair illustration of how easily this gets missed.
Type: security flaw.
What was wrong. An account told to change its password was held to that everywhere β except when signing in through single sign-on (Microsoft, Google, Keycloak and so on), which let it straight through to the app.
How it could have been used against you. It sounds harmless β the person just proved who they were at Microsoft, so why nag them for a password? But the flag is usually set because the account's local password is a widely published default. And that local password carries on working on the ordinary sign-in page regardless of how anyone signed in this time. So single sign-on was a way to use the account while quietly leaving the known-bad password in place.
What we changed. Single sign-on now respects the flag like every other route in.
Why that fixes it. The reason for the flag β a password that shouldn't be allowed to survive β doesn't go away because somebody came in through a different door.
Type: security flaw.
What was wrong. When a customer changes their portal password, FreeITSM is supposed to end any other active session on that account. The code to do that was there and was being called β but at a point in the process where it was incapable of doing anything. It failed silently: nothing was logged, and the screen cheerfully said the password had been changed.
How it could have been used against you. This matters precisely when it's most urgent. Somebody realises another person is signed in as them β a shared computer, a stolen laptop, a phished password β and changes their password to shut it down. The password did change. But the other session carried on working exactly as before. The one thing they were trying to achieve was the one thing that didn't happen, and nothing told them so.
What we changed. One line, so that the session is still open when the "end other sessions" step runs.
Why that fixes it. The step can now actually do its job. We tested it both ways to be sure: with the old code, the response contained no new session at all and the old one survived on disk; with the fix, the old session is replaced and deleted.
The equivalent screen for analysts was always correct. It was only the customer portal.
Type: feature we broke.
What was wrong. In the first round we started encrypting more of the secrets FreeITSM stores, which was the right thing to do. But encrypting them changed what's actually sitting in the database β and one screen was still reading the raw stored value and putting it on screen.
So the ready-to-copy scheduled-task address that System β Webhooks offers you contained the encrypted text instead of the token. Paste it into your scheduler and your own server answers "forbidden".
How it could have been used against you. It couldn't β this one is a broken feature, not a security hole. But it's the kind of break that wastes an afternoon, because the instructions and the software disagree and neither says why.
What we changed. That screen now decrypts the value before displaying it. We also added a command, php scripts/cron_token.php, that prints the real tokens β because four setup guides told you to read them straight out of the database, which no longer gives you anything usable. Those guides are corrected.
Why that fixes it. There's now one proper way to get a token, and the documentation points at it.
The scheduled jobs themselves were never affected β they were decrypting correctly all along. It was only the part that hands a token to a human.
Type: feature we broke. Same root cause as the one above.
What was wrong. The mail server password used for sharing articles started being encrypted, and neither side of it was told. Reading it handed the encrypted text to the mail server as though it were the password, so sending failed with an authentication error that pointed nowhere near the real cause. And saving the settings screen wrote the password back unencrypted.
That second half is what makes it nasty: the setting flipped back and forth depending on which had happened most recently. Save the form and it works; run Database Verification and it breaks; save again and it works. A problem that comes and goes is much harder to pin down than one that's simply broken.
How it could have been used against you. As a security matter, barely β but the settings screen was also sending the mail server password back to your browser in readable form, which it had no business doing.
What we changed. The password is decrypted when read and encrypted when saved, so the two halves agree. It's no longer sent back to the settings screen at all β it shows as dots, and leaving it untouched no longer overwrites it.
Why that fixes it. Both directions now do the same thing, so the value can't oscillate.
Type: feature we broke β and a genuine gap.
What was wrong. FreeITSM keeps a list of file types it will accept as attachments, which is a good and deliberate protection. That list was written with email in mind β documents and images. But the same list quietly governed the WhatsApp, Slack and web chat channels too.
Those channels mostly carry exactly the things the list didn't include. Nine of the nineteen file types our own chat code can produce were being refused: voice notes, videos, iPhone photos, shared contact cards. The feature built to receive them was throwing them away, and the customer just saw an "attachment not accepted" note.
On the email side, the ordinary clutter Outlook staples to messages β calendar invites, forwarded messages, winmail.dat β was also being refused, so a lot of perfectly normal mail arrived with a warning box on it that made FreeITSM look broken.
How it could have been used against you. It couldn't. The risk here runs the other way: someone eventually gets fed up and turns the protection off altogether.
What we changed. Audio, video, HEIC photos and the usual mail attachments are now accepted. And which types you accept is now a setting, in System β Security: leave it empty to take everything FreeITSM considers safe (including anything added by future updates), or list just the types you want.
Why this is still safe. The setting can only ever narrow the list. If you type php or exe into the box, nothing happens β it's ignored, because it isn't in FreeITSM's own set of safe types to begin with. There is no way to talk the box into accepting something executable.
And none of the newly accepted types are executable anyway. The real protection was never the shortness of the list β it's that FreeITSM renames every attachment to a name of its own choosing before storing it, so an uploaded file can't be run whatever it's called. That protection is untouched.
We proved this rather than assumed it: real voice-note, video, photo, contact-card and calendar files are now stored properly, while
shell.php, a booby-trapped.svg, a web page and a.htaccessfile are all still neutralised.
Type: bug, with permanent consequences.
What was wrong. Database Verification renames old attachments that have risky file extensions, giving them a harmless one instead. But it didn't check whether the new name was already taken β and two attachments on the same email can easily end up wanting the same new name. report.htm and report.html both become report.bin. So do index.html and index.php.
The second file silently replaced the first. Worse, both database records were then updated to point at the survivor, so there was no trace that anything had been lost.
How it could have been used against you. Not an attack β bad luck. But when it landed, the file was gone for good, and nothing said so.
What we changed. It now finds a free name instead of overwriting. The rename step also recognises several more risky extensions than it used to.
Why that fixes it. Two files can no longer become one. The name you see on the ticket is stored separately and never changes, so you'd never notice the difference.
Type: bugs in our own first-round fixes. Small, but each one made a protection weaker than it looked.
-
A stricter setting was being overwritten with a looser one. Our check for "is the sign-in cookie configured safely?" demanded one exact setting. An administrator who had chosen the stricter option failed the check β and the code then "fixed" it by replacing their stricter setting with the ordinary one. A safety check that quietly weakens a safer configuration is worse than no check. It now accepts anything at least as strong.
-
Sites behind a proxy weren't getting a protection they'd asked for. When several proxies sit in front of FreeITSM, they add to a header rather than replacing it β so it can read
https, http. We were checking for an exact match onhttps, so those sites silently missed out. We now read the first entry, which is the one that faced the visitor. -
A setting almost nobody could have known about. There's a switch that tells FreeITSM it's behind something that handles HTTPS for it. Without it, the sign-in cookie doesn't get its "only send this over a secure connection" marking. The switch appeared only in a code comment β not in any configuration file, the README, or the Docker setup. Which means the most common way people run FreeITSM in production would have missed it indefinitely. It's now documented in all three places, and still off by default, because that header can be faked by a visitor.
-
A security log that named nobody. When someone fails their two-factor code five times, we log it. The code cleared the username from memory and then tried to read it, so every one of those log entries said "unknown" β the record of repeated failed attempts identified no one, which is the only thing it's for.
Being honest about this matters more than a tidy list. Everything below is still a live problem in FreeITSM as it stands today. These are open findings that were left out of this batch of work on purpose, for reasons of sequencing β not because anyone decided they were minor. Erlend's own words when he signed the branch off: "S2 is four live cross-tenant bugs, not a hardening backlog."
-
Four more places with the same flaw as item 1 β and they are reachable right now. Erlend found four other screens with the same shape, including one that can delete a customer contact with no company check at all, and the REST API's equivalent of the screen fixed in item 1. These are not new bugs and this batch of work doesn't touch them β but neither of those things makes them harmless. Someone can use them today. We left them out because bolting an unrelated four-screen sweep onto a branch someone is reviewing makes it harder to review, not safer. This is the next piece of work, and it should not wait.
-
Two-factor code guessing. The five-attempt limit is stored in a way the attacker controls, and getting the password right resets the counters. So someone who already has a valid password can keep guessing codes for about 25% more effort than if the limit weren't there. Fixing it properly means moving the count into the database β a design change rather than a correction.
-
Cross-site request forgery. Our current protection stops the specific demonstration in the original report, but not the general problem. We've corrected our own write-up to say so, because the first draft claimed more than the code delivers. The real fix is a large piece of work across hundreds of endpoints, and it stays on the list.
-
A handful of smaller items, including that the setup page still tells anyone who visits which PHP version you're running.
Every one of the nine reported items was checked against the actual code before anything was changed β sixteen separate claims, all of which held up.
The automated test suite that came with the first round had a real weakness, which Erlend pointed out: it mostly searched the source code for text rather than running anything. That's why it certified item 5 as working while the code did nothing.
The most useful lesson from the whole exercise:
The old test checked that secrets were encrypted in the database. That passed perfectly while two features were broken β because a piece of code that never decrypts looks exactly the same from the storage side as one that works.
Checking that something was saved correctly is only half the job. You have to check it comes back out again.
The suite now does that, and runs real voice-note, video and photo files through the attachment code with checks that dangerous files are still neutralised. 131 checks pass.
- Security β how FreeITSM's protections work generally
- Security hardening 2026-08 β Developer Guide β the same nine items, technically
- Database Verification β Developer Guide β the upgrade step several of these fixes hook into
- Admin Access Control Β· Roles & Permissions
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
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
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- 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: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- 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
- β³ 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)