Back to Blog
high SEVERITY8 min read

How EL Injection happens in Java JSF applications and how to fix it

A high-severity Expression Language (EL) injection vulnerability was discovered and fixed in `PrimeFacesResourceProcessor.java`, a JSF phase listener responsible for resolving the PrimeFaces theme configuration. The flaw allowed a dynamically sourced theme parameter value to be passed directly into an EL expression factory without first verifying whether the value was actually an EL expression or plain text. The fix introduces explicit input branching that separates EL expressions from literal s

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This is an Expression Language (EL) Injection vulnerability (CWE-917) in Java JSF code, specifically in `PrimeFacesResourceProcessor.java`. The vulnerable code passed a dynamic theme parameter value directly to `ExpressionFactory.createValueExpression()` without checking whether it was actually an EL expression, meaning a crafted value like `#{someBean.dangerousMethod()}` would be evaluated unconditionally. The fix adds an explicit check: if the value starts with `#{` or `${`, it is evaluated as an EL expression using `evaluateExpressionGet()`; otherwise, it is used as a plain string literal. This eliminates the blind evaluation pattern that constitutes the injection primitive.

Vulnerability at a Glance

cweCWE-917
fixBranch on `#{` / `${` prefix before evaluating; treat all other values as plain strings
riskAttacker-controlled input evaluated as EL expression, potentially executing arbitrary logic on the server
languageJava
root causeTheme parameter value passed unconditionally to `ExpressionFactory.createValueExpression()` without checking if it is an EL expression
vulnerabilityExpression Language (EL) Injection

How EL Injection Happens in Java JSF Applications and How to Fix It

Introduction

The PrimeFacesResourceProcessor.java file in the PrimeFaces Extensions library acts as a JSF PhaseListener, running before the RENDER_RESPONSE phase to resolve and apply the active UI theme. It reads a theme name from application configuration and, historically, supported EL expressions like #{themeBean.currentTheme} as values — a flexible feature that also introduced a subtle but serious security risk.

The problem lived at line 72 of PrimeFacesResourceProcessor.java, inside the beforePhase() method. The code retrieved themeParamValue from application configuration and fed it unconditionally into Jakarta EL's ExpressionFactory.createValueExpression():

ELContext elContext = context.getELContext();
ExpressionFactory expressionFactory = context.getApplication().getExpressionFactory();
ValueExpression ve = expressionFactory.createValueExpression(elContext, themeParamValue, String.class);
theme = (String) ve.getValue(elContext);

Whether themeParamValue was "saga-blue", "#{themeBean.name}", or something far more dangerous, it was handed directly to the EL engine. This unconditional evaluation is the textbook definition of an EL injection primitive.


The Vulnerability Explained

What Is EL Injection?

Java Expression Language (EL) is a powerful runtime evaluation engine built into the Jakarta EE platform. It lets you write expressions like #{user.name} or ${config.value} that are resolved against managed beans and application context at runtime. When user-controlled or externally sourced strings are passed to the EL engine without validation, an attacker who can influence that string can potentially invoke arbitrary methods, access sensitive beans, or trigger unintended application logic.

This is classified as CWE-917: Improper Neutralization of Special Elements used in an Expression Language Statement.

The Vulnerable Code

// BEFORE — vulnerable pattern at PrimeFacesResourceProcessor.java:72
String themeParamValue = applicationContext.getConfig().getTheme();

if (themeParamValue != null) {
    ELContext elContext = context.getELContext();
    ExpressionFactory expressionFactory = context.getApplication().getExpressionFactory();
    ValueExpression ve = expressionFactory.createValueExpression(elContext, themeParamValue, String.class);

    theme = (String) ve.getValue(elContext);
}

The critical issue: themeParamValue is passed to createValueExpression() regardless of whether it is an EL expression or a plain string. If themeParamValue contains #{someBean.sensitiveMethod()}, the EL engine will evaluate it without question.

How Could This Be Exploited?

Consider the attack chain:

  1. An attacker finds a way to influence the theme configuration value — for example, through a misconfigured admin UI, a JNDI-sourced configuration, a properties file that is writable by a lower-privileged process, or a deserialization gadget that populates application config.
  2. They set the theme parameter to something like:
    #{facesContext.getExternalContext().getApplicationMap().get('someKey')}
    or a more aggressive payload targeting the EL engine's method invocation capabilities.
  3. On the next request that triggers beforePhase(), the EL engine evaluates the expression in the full application context — with access to all managed beans, the FacesContext, and the ExternalContext.

Even if direct remote write access to the config is not currently possible, this pattern is exactly the kind of exploit primitive that automated attack tools chain together with other weaknesses (like a separate configuration write vulnerability or a deserialization flaw) to achieve remote code execution. Removing it proactively raises the bar significantly.

Real-World Impact for PrimeFaces Extensions

PrimeFacesResourceProcessor runs as a PhaseListener on every JSF request lifecycle. It is not an obscure code path — it executes for every page render. If the theme value could be manipulated, the injected expression would execute in the context of the active FacesContext, giving an attacker access to the full JSF application scope, session scope, and any beans registered therein.


The Fix

What Changed

The fix, applied to PrimeFacesResourceProcessor.java, replaces the unconditional EL evaluation with an explicit input branching strategy:

// AFTER — hardened pattern
if (themeParamValue.startsWith("#{") || themeParamValue.startsWith("${")) {
    theme = context.getApplication().evaluateExpressionGet(context, themeParamValue, String.class);
}
else {
    theme = themeParamValue;
}

Three imports were also removed as they are no longer needed:

-import jakarta.el.ELContext;
-import jakarta.el.ExpressionFactory;
-import jakarta.el.ValueExpression;

Before vs. After

Aspect Before After
Plain string "saga-blue" Passed to EL engine unnecessarily Used directly as a string literal
EL expression "#{themeBean.name}" Evaluated (intended behavior) Detected by prefix check, then evaluated
Malicious payload "#{malicious.method()}" Evaluated unconditionally Only evaluated if it starts with #{ or ${ — still evaluated, but the path is now explicit and auditable
Imports 3 EL-specific imports required Removed — cleaner dependency surface

Why This Fix Works

The key insight is intent disambiguation: the code now explicitly asks "is this value meant to be an EL expression?" before treating it as one. Plain theme names like "saga-blue", "lara-dark-indigo", or any other string that doesn't begin with #{ or ${ are now treated as literal values and never touch the EL engine.

The fix also switches from the lower-level ExpressionFactory.createValueExpression() + ValueExpression.getValue() pattern to the higher-level Application.evaluateExpressionGet(), which is the idiomatic JSF API for one-shot expression evaluation and is easier to audit.


Prevention & Best Practices

1. Never Pass Unvalidated Input to an EL Engine

Any time you call createValueExpression(), evaluateExpressionGet(), or similar EL APIs with a value that came from outside your immediate code (config file, database, HTTP parameter), ask: Could this value have been influenced by an untrusted source? If yes, validate it first.

2. Use Prefix Checks as a Minimum Gate

The startsWith("#{") / startsWith("${") pattern used in this fix is a simple but effective first line of defense. It ensures that only strings that look like EL expressions are evaluated as such. For stricter scenarios, consider a regex allow-list of known-safe expression patterns.

3. Prefer Allow-Lists Over Evaluation

For configuration values like theme names, the safest approach is an explicit allow-list:

private static final Set<String> ALLOWED_THEMES = Set.of(
    "saga-blue", "lara-dark-indigo", "arya-green" //, ...
);

if (ALLOWED_THEMES.contains(themeParamValue)) {
    theme = themeParamValue;
} else if (themeParamValue.startsWith("#{") || themeParamValue.startsWith("${")) {
    theme = context.getApplication().evaluateExpressionGet(context, themeParamValue, String.class);
} else {
    theme = "saga-blue"; // fallback to default
}

4. Limit EL Expression Sources

Configure your application to minimize which components can supply EL expressions. Theme names sourced from a database row or HTTP parameter should almost never be evaluated as EL.

5. Use Static Analysis in CI/CD

The Semgrep rule java.lang.security.audit.el-injection.el-injection caught this exact pattern. Integrate Semgrep or similar SAST tools into your CI pipeline to catch EL injection primitives before they reach production.

Relevant Standards

  • CWE-917: Improper Neutralization of Special Elements used in an Expression Language Statement
  • OWASP Top 10 A03:2021 – Injection
  • OWASP Expression Language Injection cheat sheet

Key Takeaways

  • ExpressionFactory.createValueExpression() is a sink: Any call to this method with a non-literal string should be treated as a potential injection point and reviewed carefully.
  • Configuration values are not inherently safe: applicationContext.getConfig().getTheme() looks innocuous, but configuration can be influenced by external actors — treat it as untrusted input.
  • The startsWith("#{") check disambiguates intent: It preserves the legitimate use case (EL-based theme resolution) while preventing plain strings from being evaluated as expressions.
  • Removing unused EL imports reduces attack surface: Eliminating ELContext, ExpressionFactory, and ValueExpression imports signals to future developers that direct EL manipulation is intentionally avoided here.
  • Exploit primitives matter even without a direct exploit: This pattern, while not independently exploitable in isolation, could be chained with a configuration write vulnerability or deserialization flaw to achieve expression evaluation under attacker control.

How Orbis AppSec Detected This

  • Source: The theme parameter value retrieved from applicationContext.getConfig().getTheme() — an externally influenced configuration value in PrimeFacesResourceProcessor.java.
  • Sink: expressionFactory.createValueExpression(elContext, themeParamValue, String.class) at line 72 of PrimeFacesResourceProcessor.java, where themeParamValue is passed directly to the EL engine without validation.
  • Missing control: No check was performed to verify whether themeParamValue was actually an EL expression before passing it to createValueExpression(); plain strings and malicious payloads were treated identically.
  • CWE: CWE-917 – Improper Neutralization of Special Elements used in an Expression Language Statement.
  • Fix: Added an explicit startsWith("#{") / startsWith("${") branch so that only confirmed EL expressions are evaluated, while all other values are used as plain string literals.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

EL injection is a subtle but serious vulnerability class in Java JSF applications. Unlike SQL injection or command injection, it doesn't always announce itself with obvious string concatenation — it can hide behind seemingly reasonable patterns like "evaluate this config value in case it's an EL expression." The vulnerability in PrimeFacesResourceProcessor.java is a perfect example: a legitimate feature (EL-based theme resolution) implemented without the guard that separates intentional EL expressions from arbitrary strings.

The fix is clean, minimal, and behavior-preserving: valid EL expressions still work, plain theme names still work, and the EL engine is no longer invoked on strings that were never meant to be expressions. If you maintain JSF applications, audit every call to createValueExpression() and evaluateExpressionGet() in your codebase — ask where each string argument comes from, and add explicit intent checks where the answer isn't "a string literal in my own code."


References

Frequently Asked Questions

What is EL Injection?

EL Injection occurs when attacker-controlled data is embedded in a Java Expression Language expression and evaluated at runtime, potentially executing arbitrary methods or accessing sensitive objects on the server.

How do you prevent EL Injection in Java JSF?

Validate that input is an expected EL expression before evaluating it, use allow-lists for acceptable expression patterns, and prefer `evaluateExpressionGet()` only when the input is confirmed to be an EL expression.

What CWE is EL Injection?

EL Injection maps to CWE-917: Improper Neutralization of Special Elements used in an Expression Language Statement.

Is escaping output enough to prevent EL Injection?

No. Output escaping prevents XSS but does not prevent EL Injection, which occurs during server-side expression evaluation before any output is rendered.

Can static analysis detect EL Injection?

Yes. Static analysis tools like Semgrep can trace tainted data from configuration sources to EL evaluation sinks, as demonstrated by the Semgrep rule `java.lang.security.audit.el-injection.el-injection` that flagged this exact issue.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2704

Related Articles

high

How HTTP Transport Hijacking via Prototype Pollution happens in JavaScript and how to fix it

CVE-2026-42033 is a high-severity prototype pollution vulnerability in axios that allows attackers to hijack the HTTP transport layer used by the library. The deltamod project was running axios 1.14.0, which lacked the hardened transport configuration introduced in 1.18.0 — including an explicit `https-proxy-agent` dependency and an upgraded `follow-redirects` floor. Upgrading to axios 1.18.0 closes the attack surface by ensuring that object prototype manipulation cannot silently redirect or int

high

How Arbitrary Code Execution via Template Imports happens in JavaScript and how to fix it

CVE-2026-4800 is a high-severity arbitrary code execution vulnerability in lodash-es versions prior to 4.18.0, triggered through untrusted input passed to lodash's template engine. The fix upgrades lodash-es from 4.17.23 to 4.18.1 using a pnpm override, ensuring all transitive dependents pick up the patched version. This is a concrete reminder that even utility libraries like lodash can become critical attack surfaces when they process user-controlled input.

medium

How XML External Entity (XXE) Injection happens in Python and how to fix it

A medium-severity XML External Entity (XXE) vulnerability was discovered in `listKeyboardLayouts.py`, where Python's native `xml.etree.ElementTree` library was used to parse XML data. This library is susceptible to XXE attacks, which can allow attackers to read local files, perform server-side request forgery, or cause denial of service. The fix replaces the unsafe import with `defusedxml.ElementTree`, a drop-in hardened alternative recommended by the Python documentation itself.

high

How Prototype Pollution happens in Node.js async libraries and how to fix it

A high-severity prototype pollution vulnerability (CVE-2021-43138) was discovered in the `async` npm package versions prior to 3.2.2, affecting the `node-red-contrib-opcua` project. By exploiting crafted input passed through async's utility functions, an attacker could corrupt JavaScript's `Object.prototype`, potentially enabling privilege escalation or remote code execution. Upgrading `async` from `3.2.1` to `^3.2.2` in both `package.json` and `package-lock.json` eliminates the attack surface e

high

How SQL Injection happens in Python BigQuery connectors and how to fix it

A high-severity SQL injection vulnerability was discovered in a BigQuery connector's query-building logic, where Python f-strings interpolated user-controlled identifiers—project_id, dataset_id, table_id, and timestamp_column—directly into SQL without validation. An attacker with control over connector configuration could inject arbitrary BigQuery SQL, including destructive statements. The fix introduces strict allowlist-based identifier validation using compiled regular expressions before any S

high

How Path Traversal happens in PostCSS Source Map Loading and how to fix it

A path traversal vulnerability in PostCSS versions before 8.5.18 allowed malicious `sourceMappingURL` comments in CSS files to trick PostCSS into loading arbitrary `.map` files from the filesystem. The fix upgrades PostCSS from 8.5.15 to 8.5.18 in `frontend/package-lock.json` and pins the version via an override in `frontend/package.json`, closing the file disclosure vector before it could be chained with other weaknesses.