IBM Maximo is one of those platforms where XSS problems rarely come from the shiny marketing demo path. They usually come from years of customizations: automation scripts, JSP tweaks, web client extensions, integration messages rendered in the UI, and “quick” fixes that stayed in production for a decade.

If you work on Maximo, you need to think about XSS differently than you would in a small React app. Maximo mixes server-rendered UI, legacy components, rich client behavior, and lots of user-controlled business data. That combination is perfect for reflected, stored, and DOM-based XSS if output handling is sloppy.

Here’s how XSS actually shows up in Maximo, what to look for, and how to fix it without breaking the app.

Why Maximo is a common XSS trap

Maximo deployments tend to have:

  • Heavy customization
  • Multiple integration points
  • User-editable fields that get displayed across many screens
  • Legacy UI code and old habits around string concatenation
  • Admins who can configure messages, descriptions, labels, and templates

That means untrusted data doesn’t just come from “external attackers.” It often comes from:

  • Work order descriptions
  • Asset names
  • Long descriptions
  • Bulletin board or communication templates
  • Query parameters
  • Integration payloads from external systems
  • Custom dialog inputs
  • Result messages rendered after actions

If any of that data is inserted into HTML, JavaScript, attributes, or URLs without the right encoding for that context, you have XSS.

The three XSS patterns I see most in Maximo

1. Stored XSS in business objects

This is the classic Maximo issue. A user stores a payload in a field like DESCRIPTION, LONGDESCRIPTION, or a custom attribute. Later, another user opens a screen that renders it unsafely.

Example of dangerous server-side rendering in a JSP customization:

<%
String desc = mbo.getString("DESCRIPTION");
%>
<div class="asset-desc"><%= desc %></div>

If desc contains:

<script>alert('xss')</script>

that script runs in the victim’s session.

In Maximo, that can be nasty because the victim may be a supervisor or admin with broad permissions.

Safer version

For HTML body context, encode output before rendering:

<%@ page import="org.owasp.encoder.Encode" %>
<%
String desc = mbo.getString("DESCRIPTION");
%>
<div class="asset-desc"><%= Encode.forHtml(desc) %></div>

That turns dangerous characters into harmless entities.

2. Reflected XSS in custom dialogs and request handling

A lot of Maximo customizations read request parameters and push them into UI messages, titles, or form values.

Bad example:

<%
String msg = request.getParameter("msg");
%>
<div class="notice"><%= msg %></div>

An attacker sends a crafted link:

https://maximo.example.com/custom.jsp?msg=<img src=x onerror=alert(1)>

If a logged-in user clicks it, game over.

Safer version

<%@ page import="org.owasp.encoder.Encode" %>
<%
String msg = request.getParameter("msg");
%>
<div class="notice"><%= Encode.forHtml(msg) %></div>

And if the value goes into an attribute, use attribute encoding instead:

<input type="text" value="<%= Encode.forHtmlAttribute(msg) %>">

That distinction matters. I still see people use one generic “escape” function everywhere, which is how bugs survive code review.

3. DOM-based XSS in client-side extensions

Maximo environments often include custom JavaScript that manipulates the UI. This is where people reach for innerHTML because it’s quick.

Bad example:

const status = new URLSearchParams(window.location.search).get('status');
document.getElementById('banner').innerHTML = status;
```text

If `status` contains HTML or script gadgets, the browser parses it.

### Safer version

Use `textContent` for plain text:

```javascript
const status = new URLSearchParams(window.location.search).get('status') || '';
document.getElementById('banner').textContent = status;

If you truly need to render HTML, sanitize it with a trusted library like DOMPurify:

<script src="/static/js/purify.min.js"></script>
<script>
  const html = new URLSearchParams(window.location.search).get('content') || '';
  document.getElementById('banner').innerHTML = DOMPurify.sanitize(html);
</script>

My opinion: if your Maximo customization “needs” raw HTML from users, challenge that requirement hard. Most of the time it’s unnecessary and expensive to secure.

Context-aware output encoding matters

The biggest XSS mistake in enterprise apps is treating all output the same. Maximo custom code often mixes multiple contexts in one page, so you need the right encoder for each one.

HTML body

<%= Encode.forHtml(userValue) %>

HTML attribute

<input value="<%= Encode.forHtmlAttribute(userValue) %>">

JavaScript string inside a script block

<script>
  const username = "<%= Encode.forJavaScript(userValue) %>";
</script>

URL parameter

<a href="/maximo/ui?page=view&id=<%= Encode.forUriComponent(id) %>">View</a>

Using HTML encoding inside JavaScript or URLs is not enough. That’s how weird bypasses happen.

A realistic Maximo customization bug

Here’s a pattern I’ve seen in internal tools bolted onto Maximo:

<%
String wonum = request.getParameter("wonum");
String resultMessage = "Work order " + wonum + " updated successfully";
%>

<script>
  showToast('<%= resultMessage %>');
</script>

Looks harmless. It isn’t.

Payload:

?wonum=123');alert(document.cookie);//

Rendered output:

<script>
  showToast('Work order 123');alert(document.cookie);// updated successfully');
</script>

Fixed version

<%@ page import="org.owasp.encoder.Encode" %>
<%
String wonum = request.getParameter("wonum");
String resultMessage = "Work order " + wonum + " updated successfully";
%>

<script>
  showToast('<%= Encode.forJavaScript(resultMessage) %>');
</script>

Even better, avoid injecting server-side values directly into script blocks when possible:

<div id="toast-msg" data-message="<%= Encode.forHtmlAttribute(resultMessage) %>"></div>

<script>
  const msg = document.getElementById('toast-msg').dataset.message;
  showToast(msg);
</script>

That pattern is easier to reason about.

Don’t trust “internal-only” data

Maximo teams sometimes wave off XSS because “only employees use it” or “that field comes from SAP.” I don’t buy that argument.

If a field can be influenced by:

  • another internal user
  • an integration source
  • a CSV import
  • an API client
  • an email parser
  • a vendor portal

then it is untrusted.

Stored XSS in an internal enterprise app is still a serious issue because it can target privileged users. In Maximo, that often means access to work management, inventory, purchasing, vendors, and admin workflows.

Input validation helps, but it does not replace output encoding

You should validate inputs:

  • length limits
  • allowed characters where practical
  • expected formats for IDs, statuses, codes, dates

Example:

public boolean isValidWonum(String wonum) {
    return wonum != null && wonum.matches("^[A-Z0-9-]{1,20}$");
}

That reduces attack surface. But it does not eliminate XSS across all fields, especially free-text ones like descriptions and notes. Output encoding is still the real fix.

Add CSP as a backstop

A Content Security Policy won’t fix vulnerable rendering, but it can make exploitation much harder, especially for inline script payloads.

A starting point:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0m123';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'self';

Maximo customizations often rely on inline scripts, so rolling out CSP takes planning. If you’re doing that work, csp-guide.com is useful for the implementation details.

You should also verify your security headers from the outside. I usually check deployed environments with HeaderTest to make sure CSP, X-Content-Type-Options, and related headers are actually present where I expect them.

Secure coding rules I’d enforce on Maximo teams

If I were writing the house rules for Maximo customization, they’d be blunt:

  1. Never render MBO field data without context-aware encoding.
  2. Never use innerHTML with untrusted data.
  3. Never build script blocks with string concatenation from request parameters.
  4. Prefer textContent, setAttribute, and safe templating patterns.
  5. Treat integration data exactly like browser input.
  6. Sanitize only when you intentionally allow limited HTML.
  7. Add CSP, but don’t pretend it replaces code fixes.
  8. Review every legacy JSP customization as suspect until proven otherwise.

What to test during a Maximo XSS review

When testing, I’d focus on places where data gets stored and later rendered:

  • Asset descriptions
  • Work order fields
  • Long descriptions
  • Communication logs
  • Bulletin boards and announcements
  • Custom dialogs
  • Error and success messages
  • Integration-fed records
  • Search parameters echoed into the UI

Payloads don’t need to be fancy. Start simple:

<script>alert(1)</script>

Then try attribute-breaking payloads:

" onmouseover="alert(1)

And JavaScript string breakers:

');alert(1);//

The payload depends on the context. That’s the whole point.

Final thought from the trenches

Most XSS in IBM Maximo is self-inflicted through customization, not some magical platform-specific exploit. The platform gives you plenty of ways to shoot yourself in the foot if you mix untrusted data with HTML and JavaScript carelessly.

The fix is not glamorous: encode correctly, stop using unsafe DOM APIs, validate inputs sensibly, and deploy CSP as a safety net. That’s the work. It’s boring, but it’s how you keep a heavily customized Maximo environment from becoming an attacker’s pivot point.