PHP RFC: Limit maximum number of filter chains
- Version: 1.3
- Date: 2026-05-05
- Author: Sjoerd Langkemper sjoerd-php@linuxonly.nl
- Status: Under discussion
- Implementation: https://github.com/php/php-src/pull/22110
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:
- In local file inclusion
include $_GET['a'], this makes it possible to include arbitrary code instead of just files on the disk of the server. - In file reads
file($_GET['a']), it makes it possible to determine the contents of the given file, even if the application does not display the content. An attacker can extract information about the file by triggering content-dependant errors in the filter chain. - To pass format validation. E.g. an attacker uses a filter chain to change /etc/passwd into a sufficiently valid image file, so that it passes image validation performed by the application.
- To exploit vulnerabilities in iconv, or other filter functionality.
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:
- 1 filter: prevent PHP from interpreting an included file, returning its contents to the user instead of running the code.
- 5 filters: error oracle to determine whether a file starts with a hex character.
- 11 filters: exploit an iconv vulnerability, non-blind.
- 47 filters: prepend '%PDF' before /etc/passwd
- > 50 filters: error oracle that returns contents of a file.
- > 100 filters: create a stream that returns a webshell.
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)
- PHP 8.6: deprecation warning when using more than 16 filters, error when stream context option
filter.max_filter_countis set. - PHP 8.7: error as described in PHP RFC: Stream Error Handling Improvements
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
- Exact Limit: Should the default be 10, 16, 32? 16 seems like a good trade-off between security and allowing legitimate use.
- Hardcode the limit or add a PHP INI setting? What should the INI name be? filter.max_chain_length? Jakub Zelenka suggested a stream context option, which seems well suited for this.
- How should this be introduced and tightened? E.g. start with high hard limit, or low limit and give a deprecation warning instead of an error? With a deprecation warning, as this is technically a backwards incompatible change.
- Should exceeding the limit throw a ValueError (consistent with modern PHP 8 APIs) or a Warning (consistent with legacy stream handling)? First a deprecation warning, then as described in the PHP RFC: Stream Error Handling Improvements
Future Scope
- Disallow filter URLs in include/require, if allow_url_include is not set.
- Change behaviour or accessibility of commonly abused filters such as
dechunk. - Deprecate the use of strings for complex filter definitions in favor of an object-oriented API for stream manipulation.
- Prevent against attacks that use few filters.
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:
- Sjoerd Langkemper (me) originally proposed a limit of 50 or 100.
- Julien Voisin suggested a limit of 3 or 5.
- Derick Rethans objected to this but thinks a limit of 16 is acceptable.
- Jakub Zelenka suggested using a stream context option to make the limit configurable.
What should happen when the limit is reached?
- Derick Rethans and PR 16699 suggest throwing an exception, but Jakub Zelenka correctly points out that this “inconsistent with current stream error handling”.
- Since this is technically a BC break, it may be worth it to generate a warning before setting a hard limit.
- Jakub Zelenka has done work on a constistent streams error API, which takes away the dilemma between warnings and exceptions.
Voting Choices
Implement limiting the number of Filter Chains in php://filter?
Primary Vote requiring a 2/3 majority to accept the RFC:
Patches and Tests
There's already an existing merge request, and I am willing to do some work on this.
Implementation
After the RFC is implemented, this section should contain:
- the version(s) it was merged into
- a link to the git commit(s)
- 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
- 0.1 2026-05-05 Initial draft
- 0.2 2026-05-08 Added iconv vulns, dechunk behaviour change
- 0.3 2026-05-09 Added “Number of filters” section
- 1.0 2026-05-09 Under discussion
- 1.1 2026-05-15 Added “Discussion” section
- 1.2 2026-05-19 Make clear that stream_filter_append is unaffected
- 1.3 2026-05-21 New PR, more firm suggestions
- 1.4 2026-05-22 Change limit through stream context option