-
Notifications
You must be signed in to change notification settings - Fork 15
Content Security Policy Hardening
Note
Status: PARKED / documented. This page records what it would take to make FreeITSM run under a strict Content Security Policy, why that's a large piece of work rather than a bug fix, and an honest, measured assessment of the actual risk. It also captures the CSP that works today, which resolves the practical need for almost everyone. Raised as GitHub issue #43 by a security-conscious self-hoster.
Background reading: Email Rendering & Images touches the same theme (untrusted HTML, isolation, defence in depth).
A Content Security Policy (CSP) is a rule you give the browser that says, in effect, "only run scripts and load resources from places I explicitly trust." It's a seatbelt: if an attacker ever manages to sneak malicious code onto a page, a good CSP stops the browser from actually running it.
A self-hoster set a strict CSP on their web server and found parts of FreeITSM stopped working β first the Database Verify button on the setup page, then switching between modules. Their strict policy said "only run script files served from this site, and nothing written inline in the page." Loosening it (allowing inline script) fixed everything.
That's not a coincidence or a one-off bug. FreeITSM β like WordPress, phpMyAdmin, and the large majority of server-rendered PHP applications β was built with a lot of inline scripting: little <script> blocks written directly into pages, and onclick="β¦"-style handlers written directly onto buttons. A strict CSP forbids exactly that. So a strict policy doesn't break two features β it breaks the inline pattern the whole UI is built on, and those two were simply the first the reporter hit.
A quick audit of the current codebase:
| Thing a strict CSP forbids | Count | Notes |
|---|---|---|
Inline event handlers in PHP (onclick= β¦) |
~1,088 across 122 files | The visible buttons/links |
| Inline handlers baked into JS-rendered HTML | ~200 more | The reading pane, settings modals etc. build HTML strings with onclick= in them β harder to find, only "fire" when that view renders |
Files with inline <script> blocks |
159 | Page bootstrap, config, the t() i18n bridge, per-page glue |
Inline style="β¦" attributes |
~1,444 | The style-src surface β see the note below |
Uses of eval() / new Function() in app code |
0 | Good news β see below |
| CSP shipped by the app itself | none | The app is CSP-agnostic; the reporter added one at the Apache vhost level |
So the inline script surface alone is roughly 1,300 handlers + 159 script blocks, and the inline style surface is larger still.
It's worth being clear-eyed here, in both directions.
Why it genuinely matters. CSP is a real, respected layer of defence. Its value is defence in depth against cross-site scripting (XSS): if some input ever slips through un-escaped and injects a <script>, a strict CSP is the safety net that stops it executing. FreeITSM does handle untrusted input β inbound email HTML, requester-portal submissions, form fields, ticket content β so the class of threat CSP mitigates is not hypothetical for this kind of app.
Why it is not an emergency. A CSP is the second line of defence, not the first. The first line is correct output encoding, and FreeITSM already does that pervasively (htmlspecialchars throughout; inbound email is parsed and sanitised, then rendered in an isolated shadow root β see Email Rendering & Images). CSP would harden the app if an escaping gap existed; it is not itself the thing standing between you and compromise. FreeITSM is also self-hosted and internal β an IT team's own service desk behind their own auth β not a public, anonymous, multi-tenant SaaS where the blast radius of one XSS is enormous.
The measured conclusion. Strict-CSP support is a legitimate hardening goal with low-to-moderate real-world urgency for a typical deployment. That balance matters, because the "fix" is a very large, cross-cutting change β and a rushed refactor of ~1,300 interactive handlers is itself a reliable way to introduce bugs (and bugs in security-sensitive UI can be worse than the gap you set out to close). The responsible position is: take it seriously, give people a working policy now, and treat full strict-CSP as a deliberate, phased project to be done well or not at all β not as a reactive scramble.
You do not need any of the below to run a sensible CSP right now. Because the app's own code contains no eval, the 'unsafe-eval' that the reporter carried over from phpMyAdmin's policy is almost certainly unnecessary. A good working policy is:
Content-Security-Policy:
default-src 'self' data:;
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self';
frame-src 'self' https://www.youtube.com;
base-uri 'self';
object-src 'none';
-
script-src 'self' 'unsafe-inline'β required by the inline pattern described above. Dropping'unsafe-eval'is worth testing and will likely work everywhere except possibly a specific rich-media page (a bundled library such as the editor or a diagram/export tool might want it β verify per feature rather than granting it globally). -
style-src 'unsafe-inline'β required by the ~1,444 inline styles (and by libraries that inject styles). -
img-src β¦ https:β inbound emails and knowledge articles can hot-link remote images. -
frame-src β¦ youtube.comβ only if you embed the videos.
This is the same shape of policy WordPress and phpMyAdmin ship, and it is a perfectly defensible posture. The honest documentation line is: "FreeITSM supports a CSP with 'unsafe-inline'; a strict policy that forbids inline script is a known limitation on the roadmap."
If someone wants to pursue real strict-CSP support, here's the shape of the terrain, including the parts that surprise people.
Nonces don't rescue you. The obvious move β a per-request nonce on each <script nonce="β¦"> and script-src 'self' 'nonce-β¦' β legitimises the 159 inline <script> blocks, but CSP cannot nonce an inline event-handler attribute. There is no onclick nonce. So a nonce strategy still requires removing every one of the ~1,300 onclick=-style handlers first. Nonces are necessary but nowhere near sufficient.
Hashes are impractical here. script-src 'sha256-β¦' can whitelist a static inline block by its hash, but the blocks are numerous, varied, and often template inline values (so their hash changes per render). Not a realistic path for 159 blocks.
The tractable migration is delegation, not 1,300 rewrites. Rather than converting each handler to its own addEventListener, the sane pattern is a data-attribute + delegated dispatcher: mark up <button data-action="openNoteModal" data-id="42"> and register one listener per view that reads data-action and calls the matching function. That collapses ~1,300 individual edits into a mechanical find-and-replace plus a small dispatch table β far lower risk and far easier to review. The JS-rendered handlers (the ~200 baked into template strings) get the same treatment at their source.
report-only is how you do this without breaking anything. Ship Content-Security-Policy-Report-Only with the strict policy and a report-to/report-uri endpoint. The app keeps working (report-only doesn't block), but every violation is reported, giving an empirical, exhaustive list of what still needs converting β including the JS-injected handlers that only fire when a particular view renders and which static grep can miss.
Style is the harder half, and probably not worth chasing. There are more inline style= attributes (~1,444) than inline scripts, and β exactly like event handlers β inline style attributes cannot be nonced. Removing them means moving every inline style into stylesheets or data-driven classes, an even bigger sweep with far less security payoff (CSS-only injection is a much weaker vector than script injection). The pragmatic, widely-accepted target is therefore strict script-src (nonce-based, no unsafe-inline) while keeping style-src 'unsafe-inline'. Chasing strict style-src is diminishing returns.
Realistic end state: script-src 'self' 'nonce-β¦' (no unsafe-inline, no unsafe-eval), style-src 'self' 'unsafe-inline', plus the resource directives above. That is a genuinely strong posture and an achievable one; fully strict everything is not a sensible goal for an app of this shape.
-
Baseline + measurement. Ship a sensible default CSP header from the app (the working policy above), documented. Simultaneously offer a
report-onlystrict variant with a violation-collection endpoint, to build the real inventory. -
De-inline
<script>blocks. Move page-bootstrap data (API_BASE, config, i18n payload) intodata-*attributes or a single JSON island read by external JS; add a per-request nonce to whatever inline script genuinely must remain. -
De-inline handlers, module by module. Convert to the
data-action+ delegated-dispatcher pattern, one module at a time, while still in report-only so nothing breaks in production. Track progress per module (this is 21 modules of surface). -
Flip to enforcing, per module. Once a module reports clean under report-only, it can move under an enforcing strict
script-src. Rollout is incremental, not big-bang. -
Leave
style-src 'unsafe-inline'. Document it as a deliberate, pragmatic decision.
-
The JS-injected handlers are the trap. De-inlining the PHP is the easy 80%; the ~200 handlers built inside JS template strings (reading pane, settings modals, pickers) are the hard 20% β they only violate when that view renders, so they slip past static analysis.
report-onlytelemetry is the only reliable way to find them all. - Nonce plumbing touches everything. A per-request nonce must be generated once and threaded through every shared header/include, and interacts with any page/output caching. Get it wrong and either scripts break or the nonce becomes predictable (which defeats it).
-
Third-party libraries. The rich-text editor, charting, and diagram/export tooling may inject inline styles/scripts or want
evalon specific pages. These need testing per feature; a global grant is the wrong answer. - Regression surface. ~1,300 interactive handlers is a lot of buttons; every conversion is a chance to silently break one. This work demands the project's verify-the-change discipline, ideally with click-through coverage per module.
- Half-migrated confusion. An app where some modules are strict and some aren't needs clear tracking, or it becomes impossible to reason about what policy is safe to enforce.
-
Don't over-reach on
style-src. It's tempting to "finish the job", but strict style is a much bigger sweep for a much smaller gain. Scope discipline matters.
This is worth doing only as a deliberate, report-only-guarded, per-module project with real demand behind it β and the realistic target is strict script-src, not strict everything. Absent that, the correct state is parked, with the working 'unsafe-inline' policy clearly documented so security-conscious admins know exactly what is and isn't supported. Parking it indefinitely is an acceptable outcome; the documented policy meets the practical need for the overwhelming majority of deployments.
- Blue sky thinking β the holding area this idea lives in
- Email Rendering & Images β untrusted HTML, sanitisation, shadow-DOM isolation (the same defence-in-depth theme)
- Security β encryption and access control
- GitHub issue #43 β the original report
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)