Table of Contents

PHP RFC: Limit maximum number of filter chains

Introduction

This RFC proposes to introduce a limit on the number of filters that can be chained within a php://filter URL. The primary goal is to mitigate a class of security vulnerabilities where Local File Inclusion (LFI) is elevated to Remote Code Execution (RCE) via complex, generated filter chains.

Currently, an attacker can use a long sequence of convert.iconv.* and convert.base64 filters to craft arbitrary content (like a PHP shell) out of any file on the disk, or even from an empty resource. By limiting the chain length to a reasonable number (e.g., 16), we can effectively disable these automated exploitation tools while maintaining support for legitimate use cases.

Attacks

PHP filters are a way of transforming streams, for example through base64 encoding. By using many filters, it is possible to transform any content, such as an empty resource, into any other content, such as a webshell. This is mainly useful in these situations:

These are well documented actual attacks, but filter chains are sufficiently powerful that they are likely helpful in exploiting other vulnerabilities.

Proposal

The proposal seeks to limit the number of individual filter components allowed in a single `php://filter` string. This limit applies specifically to the parsing of php://filter URLs (e.g., via fopen, include, file_get_contents). When the limit is exceeded, PHP should give a deprecation warning, and later an error.

The limit will default to 16, and can be changed using the stream context option filter.max_filter_count. There will not be a PHP INI setting for this. The limit is sufficiently high that legitimate users are unlikely to run into this limit.

Using many filters will still be possible using stream_filter_append / prepend.

Examples

Still works:

var_dump(file_get_contents("php://filter/string.toupper|string.rot13/resource=data://text/plain,hello"));
// string(5) "URYYB"

Deprecation warning:

file_get_contents("php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|...[48 more].../resource=php://temp");
// Deprecated: Using more than 16 filters in a php://filter URL is deprecated, set this limit using the stream context option max_filter_count, or use stream_filter_append in ...

Error (when filter.max_filter_count is set, or in later PHP version):

var_dump(file_get_contents("php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|...[48 more].../resource=php://temp"));
// Warning: file_get_contents(...): Failed to open stream: too many filters in ...
// bool(false)

Number of filters

A survey of open-source code (Sourcegraph) indicates that the vast majority of php://filter usages involve 1 filter, and occasionally 2. Legitimate use cases for 5 or more filters were not found outside of CTF challenges and exploit payloads.

Here are some numbers for malicious use:

For the last two, improvement is still possible. I.e. attackers can come up with better attacks that use fewer filters.

Backward Incompatible Changes

This is a backward incompatible change for any application that legitimately chains more filters than the defined limit.

There is a clear upgrade path for users who want to use many filters on a stream: configure the stream context option filter.max_filter_count, or use stream_filter_append / prepend.

Proposed PHP Version(s)

RFC Impact

To the Ecosystem

Most developers will notice no change. Security scanners and WAFs may be simplified as they can rely on the engine's built-in protection.

To Existing Extensions

Minimal to none, as this logic resides within the core PHP stream wrapper for php://.

To SAPIs

No specific impact to CLI, FPM, or others.

Open Issues

Future Scope

Discussion

Reception is generally positive, but people disagree on the amount of filters that should be allowed and what exactly should happen when that limit is exceeded.

About the limit:

What should happen when the limit is reached?

Voting Choices

Implement limiting the number of Filter Chains in php://filter?

Primary Vote requiring a 2/3 majority to accept the RFC:

Implement $feature as outlined in the RFC?
Real name Yes No Abstain
Final result: 0 0 0
This poll has been closed.

Patches and Tests

There's already an existing merge request, and I am willing to do some work on this.

https://github.com/php/php-src/pull/16699

Implementation

After the RFC is implemented, this section should contain:

  1. the version(s) it was merged into
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature

References

Rejected Features

Keep this updated with features that were discussed on the mail lists.

Changelog