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:
- 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.
- 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. - On the next request that triggers
beforePhase(), the EL engine evaluates the expression in the full application context — with access to all managed beans, theFacesContext, and theExternalContext.
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, andValueExpressionimports 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 inPrimeFacesResourceProcessor.java. - Sink:
expressionFactory.createValueExpression(elContext, themeParamValue, String.class)at line 72 ofPrimeFacesResourceProcessor.java, wherethemeParamValueis passed directly to the EL engine without validation. - Missing control: No check was performed to verify whether
themeParamValuewas actually an EL expression before passing it tocreateValueExpression(); 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
- CWE-917: Improper Neutralization of Special Elements used in an Expression Language Statement
- OWASP Expression Language Injection
- OWASP Injection Prevention Cheat Sheet
- Jakarta EE
Application.evaluateExpressionGet()Documentation - Semgrep Rule: java.lang.security.audit.el-injection.el-injection
- harden: an expression is built with a dynamic value in...