Skip to content

Security Hardening 2026 08 Round Three

Ed Mozley edited this page Aug 12, 2026 · 3 revisions

Security hardening, August 2026 β€” round three, in plain English

When the second round of security fixes was signed off and merged, ten problems were left behind β€” nine of them named, and one described only as a category, which is why it was the one almost missed. They were not forgotten and they were not judged harmless: they were held back because bolting unrelated work onto a branch somebody was reviewing makes it harder to review, not safer.

This page is what happened when we went back and did them. It is written for someone who runs FreeITSM rather than someone who writes it β€” there is a companion page, Security hardening 2026-08 β€” Round three Developer Guide, that says the same things with file names, proof-of-concept requests and code.

βœ… Status: merged. All of them are fixed on main (d26f9bbb, plus 980b0939). Not yet in a tagged release.

⚠️ This is not everything. Two large pieces are still open by design β€” see What is still outstanding at the bottom. Please read it rather than assuming this page closes the subject.

The reporter. All of these were found by Erlend Volden, who audited FreeITSM privately in August 2026 and then re-reviewed our fixes. The first round is on the Security page; the second is on Security hardening 2026-08.


How to read this

Every entry answers the same four questions:

  • What was wrong β€” the mistake itself.
  • How it could have been used against you β€” what somebody could actually have done.
  • What we changed.
  • Why that fixes it.

The first four all share a single theme, so it is worth stating once up front.

FreeITSM can host several customer companies side by side, and analysts are normally restricted to the ones they look after. Every one of the first four problems is a place where that restriction was not applied. None of them was exotic. Each was simply a screen or an interface where nobody wrote the check.


1. πŸ”΄ Deleting a customer contact checked nothing at all

Type: security flaw. The most clear-cut one here.

What was wrong. The screen that deletes a customer contact had no company check of any kind. Not a weak one β€” none. Any signed-in analyst with access to the Tickets module could delete any contact on the whole installation.

How it could have been used against you. An analyst who looks after Company A could delete Company B's contacts. On a shared installation that is somebody else's customer records disappearing.

There was a quieter half to it as well. Before deleting, FreeITSM checks whether the contact is attached to any tickets, and refuses if they are β€” "Cannot delete: this user is the requester on 14 ticket(s)." That refusal was calculated without any company check either. So even when the deletion was blocked, the refusal message truthfully reported how much work another company had attached to that person. Ask about contact after contact and you have counted a competitor's workload, without ever being allowed to touch any of it.

What we changed. The company check now runs before anything is counted, and the refusal is now the same words used for a contact that does not exist: "User not found."

Why that fixes it. Putting the check in the file would not have been enough β€” if it had gone underneath the counting, the counting would still have happened and the message would still have leaked. It had to go above. And making "you are not allowed" indistinguishable from "there is nothing there" means the refusal itself gives nothing away.


2. πŸ”΄ The programming interface could read and change any customer record

Type: security flaw.

What was wrong. FreeITSM has an API β€” a way for other software to talk to it β€” and each API key can be restricted to particular companies. For customer records, that restriction was not applied in three places: listing them, reading one, and modifying one.

How it could have been used against you. A key issued to one customer company could page through every requester in the entire installation, complete with names and email addresses, or search them. It could also modify any of them, which is worse than it sounds: change somebody's email address to one you control, then use the customer portal's "forgotten password" feature against the address you just set, and you have their account.

What we changed. All three now respect the key's company restriction. Reading or modifying a record outside it gives the same "not found" answer as a record that does not exist.

Why that fixes it. The interface was already careful in other places β€” creating a contact checked which company it would land in β€” which is exactly why this was easy to miss. The file looked company-aware. Only three specific operations were not.

Worth knowing. The original report named only the "modify" case. The listing and reading cases were found while fixing it, and the listing case is the bigger leak of the three. Where a report names one instance of a mistake, the rest of the file is worth reading.


3. πŸ”΄ "Preview before escalating" showed another company's ticket

Type: security flaw. In terms of what actually came back on screen, this is the most revealing one on the page.

What was wrong. When you escalate a ticket to an external tracker like Jira, FreeITSM offers a preview first β€” this is what we are about to send, are you sure? β€” because you cannot unsend it. That preview was built and returned before the company check was reached.

The check existed. It lived inside the shared code that does the actual escalating, and the design rule was "every check lives in that shared code". That rule held perfectly for sending. The preview stops short of it.

How it could have been used against you. Ask for a preview of any ticket number at all, and FreeITSM returned that ticket's subject line, the requester's name and email address, its priority and type, the text of the message the customer originally wrote, and the filename of every attachment. No external tracker needed to be set up, because the preview never touches one.

What we changed. The company check now happens at the entrance, before anything is read, so it covers previewing and escalating alike.

Why that fixes it. "Every check lives in the shared code" is a good rule that quietly covered only half the paths. A rule that is true for the way you usually go through a file is not the same as a rule that is true.


4. 🟠 The messaging test screens would test another company's channel

Type: security flaw.

What was wrong. Two diagnostic screens β€” "test this messaging channel" and the more detailed Slack health check β€” checked whether you were allowed to administer channels in general, but not whether you were allowed to administer that channel.

How it could have been used against you. An analyst restricted to one company could point these at a channel belonging to another, validating that company's stored credentials against Slack or WhatsApp, reading back their workspace name, their bot's identity and their configured addresses. The "simulate a message" mode would go further and push a synthetic message through their live intake to create and then delete a ticket in their company.

What we changed. Both now check the specific channel.

Why that fixes it. The rule this needed was already written down elsewhere in FreeITSM, including the subtle part: a channel that is not pinned to any company is shared intake β€” a front door for everybody β€” rather than something owned by the default company. Shared channels stay open to anyone with the right permission. Only a channel pinned to one company is restricted. Neither screen was calling that rule; both now do.


5. πŸ”΄ Guessing a two-factor code was effectively unlimited

Type: security flaw. This one is worth reading slowly, because the original reasoning sounded convincing and was wrong.

What was wrong. When you sign in with two-factor authentication, FreeITSM allows five wrong codes before abandoning the attempt. That count was kept in your browser session β€” and the session belongs to whoever is doing the guessing. Throw it away, and the count is gone.

The previous note in the code argued that this was fine: to get a new session you have to enter the password again, and the password step is protected by account lockout.

Here is why that was wrong. A successful password entry clears the lockout counters. Someone who already has a valid password β€” from a leak, or reused from another site β€” never trips the lockout at all. Every loop starts with a success.

How it could have been used against you. Enter the correct password, guess five codes, throw the session away, repeat. A six-digit code is a million possibilities, and the extra cost of that loop is one additional request per five guesses β€” about 20%. The second factor was, in practice, not a second factor for anyone who already had the password.

What we changed. The count now lives on the account itself, where the person guessing cannot reach it. It survives throwing the session away, it survives entering the password again, and the only thing that clears it is entering a correct code. When the limit is reached the code step is locked for the same period as your account lockout policy. It applies to analyst and customer-portal sign-in alike.

Why that fixes it. The count is no longer stored somewhere the attacker controls, and it is deliberately kept separate from the ordinary password counters so that a correct password cannot reset it. That separation is the fix.

You may need to run Database Verification. This adds two new columns. Until you do, FreeITSM falls back to the old browser-session count β€” no worse than before, but not yet fixed. It deliberately does not refuse all sign-ins in the meantime, because locking out everybody who uses two-factor authentication would be a worse outcome than the problem.


6. 🟠 An SVG could be uploaded as your logo

Type: security flaw.

What was wrong. The branding screen accepted SVG logos. An SVG is not really a picture β€” it is a document, and it can contain a program.

How it could have been used against you. Inside the <img> tags where a logo normally appears, that program stays dormant, so the logo itself looked harmless. But the file sat at a predictable web address on your site, and opening it directly makes the browser treat it as a page rather than a picture β€” at which point the program runs, with your site's full authority behind it.

What we changed. SVG is no longer accepted. PNG and JPG are unaffected. The uploaded file is also now stored under a random name that FreeITSM chooses, rather than a predictable one.

Why that fixes it. Every other part of FreeITSM had already stopped accepting SVGs for exactly this reason. The branding screen had its own separate list of allowed types written before that decision, and so never got the message. It now uses the same shared list as everything else β€” which is the real fix, because one exception is how a rule stops meaning anything.

⚠️ If you already use an SVG logo, it keeps working β€” and it is still an SVG. This change stops a new one being uploaded; it cannot make an existing one safe. Replacing it with a PNG is worth doing.


7. 🟑 Three smaller things, same afternoon

  • The setup page told strangers your exact PHP version. That page has to be reachable without signing in β€” otherwise you could never check a fresh installation β€” and it printed the precise version number to anybody who asked. That single string is the difference between "there is a system here" and a published list of known weaknesses to try against it. It now says whether your version is fine, ageing or too old, without the number; sign in as an administrator to see the number. Every other page was quietly announcing the same thing in a hidden header, which has also been removed.

  • The merge snapshot was written by hand. When you merge two tickets, FreeITSM keeps a copy of the closed ticket's conversation as a web page. That file was written straight into the attachments folder without going through the checks every other attachment passes. It now goes through them, and is stored in a form no web server will ever run. You download and open the same readable file as before.

  • Uploaded files were trusted when they could not be checked. FreeITSM verifies that a file's contents match its name, so a program renamed to .png is caught. On a server missing a standard PHP component, that check quietly did nothing at all and the file was accepted on the strength of its name alone. It now refuses the upload and tells the administrator which component to enable.


8. 🟠 One more guard that trusted a busy database

Type: security flaw. Found while checking whether this page was telling the truth, which is the only reason it is here.

What was wrong. Round two fixed a family of checks that "failed open" β€” that is, when something went wrong while working out whether you were allowed to do something, they answered yes. One was missed. The check that decides whether you may reach a ticket's tasks answered yes whenever its database query threw any error at all.

The note beside it said "column missing on a part-migrated install", which is a real situation and a fair thing to allow for. The problem is that a query throws for many reasons: the database being briefly busy, a dropped connection, a permissions problem. All of them were being read as this person is allowed.

How it could have been used against you. Not by a clever request β€” by load. Under a slow moment on a busy database, an analyst or API key restricted to one company could read another company's tasks. Intermittent, invisible, and impossible to reproduce on a quiet system.

What we changed. It now uses the same helper as every other check of its kind: a genuinely missing column still allows, and every other error refuses and writes a line to the log.

Why that fixes it. The intention was never wrong β€” part-migrated installations do need to keep working. The implementation forgave every error rather than the one it meant to. Round two built the helper that draws exactly that line; this check simply was not using it.

How it was missed. The round-two outstanding list named this class as "S3's cousins" rather than naming a file. Working through that list, every item with a filename got done and the cross-reference did not. The lesson is small and worth keeping: an entry on a to-do list that does not name a thing is the entry that gets skipped. There is now an automated check that sweeps for this pattern, so a future one cannot be added quietly.


9. 🟑 A protection that broke the site instead of protecting it

Type: a bug in one of our own earlier fixes.

What was wrong. To stop uploaded files being run on servers using Microsoft IIS, we added a configuration file to each upload folder that removed the ability to run programs there. On a default IIS installation, that particular setting is not one a folder is allowed to change β€” so IIS refused to read the file at all and answered HTTP 500.19 for everything underneath it.

How it could have gone wrong for you. Not as a break-in. As a broken site: an error page where your attachments should be, that reads like FreeITSM being at fault. And a folder that returns an error has not been secured β€” it has been broken, which is a different thing and often a slower thing to diagnose.

What we changed. That setting is gone from all five folders and from the code that creates new ones. The other rule in the same file already did the whole job, is allowed at folder level, and returns a clean refusal.

Why that fixes it. The protection was never the part that was removed. It was doing nothing except causing the error.


10. 🟠 Database Verification said it never deletes anything. It does.

Type: a false statement in our own documentation. No attacker required β€” this one could have cost you data.

What was wrong. The note inside Database Verification said it "is idempotent and never drops anything". It was not true, and had not been for a long time. Verification removes seven outdated columns β€” old ticket status and priority fields, old requester name and email fields, a legacy supplier name, and two War Room fields β€” each at the end of a migration that has already copied the information somewhere better.

How it could have gone wrong for you. "The data was moved first" and "nothing is ever removed" are very different promises. Anyone who read that note had no reason to take a backup before pressing the button. If a migration only partly succeeded, the backup is the difference between a hiccup and a loss.

What we changed.

  • The note now says plainly that verification creates, alters and drops, and tells you to take a backup.
  • The page itself now carries the same warning, where an administrator will actually see it.
  • There is a new Preview button. It inspects your database and tells you exactly what a verification would create, add, or permanently remove β€” and changes nothing at all.

Why that fixes it. The problem was never that verification removes things; that is usually the right thing for it to do. The problem was that the one sentence somebody would read before deciding whether to take a backup was telling them the opposite of the truth.

A note on what Preview is. It inspects your database and reports differences. It is not the migration running with a safety catch on β€” that is genuinely not possible here, because database structure changes cannot be undone by the usual "try it and roll back" mechanism. What it gives you is an accurate answer to "is there anything destructive waiting for me?" before you commit.


What is still outstanding

Everything reported in scope for this round is done. These are not. They are open, several are live in the current code, and they are listed here so that nobody reads this page as the end of the story.

  • Cross-site request forgery. The current protection stops the specific demonstration in the original report but not the general problem. The real fix spans several hundred places and is a piece of work in its own right. Still open.

  • SCORM course packages (lms/content) are served in a way that has not been hardened. Unlike the branding folder, this one cannot simply be locked down: SCORM courses are web pages with programs in them, and preventing those programs from running would stop courses playing at all. It needs a different answer β€” most likely serving them from a separate address β€” and that is a design job rather than a setting. Still open, deliberately.

  • An SVG logo already uploaded stays as it is. See item 6 β€” replacing it is an operator action.

  • SLA snapshotting and subject access / erasure are features rather than fixes, and are not scheduled here.


Related pages

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally