# Path traversal substring filter bypass

Path guards that test for a substring, dot, or extension instead of canonicalized containment

_Area: Web injection and code execution. Seen in 3 disclosed reports; one confirmed bounty: $1,500; latest 2022-10-14. Updated 2026-10-24._

## How it works
A path guard tries to prove a path is safe by scanning its bytes for a marker: a required substring, a `..` sequence, or an extension. The sink (file open, include, write) then resolves that same string under different rules. The guard's assumption — that "contains the marker" or "contains no `..`" means "stays inside the intended directory" — is false. Any gap between how the filter reads bytes and how the filesystem or interpreter resolves them becomes the bypass.

## Where it shows up
- Guards using `strpos`/`strstr`/`stristr` to require a substring like `data_products` or reject `..` [E2](https://bugbountylibrary.com/r/blog%3Apwn-ai-blog-cve-2021-45467-cwp-centos-web-panel-preauth-rce.md)[E3](https://bugbountylibrary.com/r/h1%3A1626210.md)
- Extension checks matching a pattern anywhere in the path rather than as a true suffix [E1](https://bugbountylibrary.com/r/h1%3A458842.md)
- `include()`/`require()` or file-write sinks downstream of a string filter [E2](https://bugbountylibrary.com/r/blog%3Apwn-ai-blog-cve-2021-45467-cwp-centos-web-panel-preauth-rce.md)
- Null bytes or case-folding where the filter counts bytes the OS ignores [E2](https://bugbountylibrary.com/r/blog%3Apwn-ai-blog-cve-2021-45467-cwp-centos-web-panel-preauth-rce.md)
- Traversal sequences appended after a required marker [E3](https://bugbountylibrary.com/r/h1%3A1626210.md)

## How to review
Find every path guard and its sink. The invariant: after canonicalization (resolve `..`, strip nulls, normalize case), the resolved absolute path must still sit inside the allowed base directory, and the extension must match as a true suffix. Check that the guard canonicalizes before comparing, not after. A substring presence test, a `..` substring test, or a mid-string extension match does not establish containment. Confirm filter and sink apply the same normalization.

## Variants seen
- Extension check matching `.HL?` anywhere in the path, so an embedded marker permits arbitrary extensions [E1](https://bugbountylibrary.com/r/h1%3A458842.md)
- Null-byte `.%00./` passing a `stristr` dot filter while `include()` resolves it as `../` — a filter/sink byte-counting mismatch [E2](https://bugbountylibrary.com/r/blog%3Apwn-ai-blog-cve-2021-45467-cwp-centos-web-panel-preauth-rce.md)
- Required marker kept at the start, traversal appended to escape the base dir — a containment failure, not a dot-filter bypass [E3](https://bugbountylibrary.com/r/h1%3A1626210.md)

## Impact and severity
Led to arbitrary file read of sensitive files and source [E3](https://bugbountylibrary.com/r/h1%3A1626210.md), local file inclusion chained to preauth root RCE [E2](https://bugbountylibrary.com/r/blog%3Apwn-ai-blog-cve-2021-45467-cwp-centos-web-panel-preauth-rce.md), and arbitrary file write with DLL loading [E1](https://bugbountylibrary.com/r/h1%3A458842.md). Severity rises sharply when a write primitive or include sink is reachable, turning a read into code execution.

## Evidence
- E1: https://bugbountylibrary.com/r/h1%3A458842.md
- E2: https://bugbountylibrary.com/r/blog%3Apwn-ai-blog-cve-2021-45467-cwp-centos-web-panel-preauth-rce.md
- E3: https://bugbountylibrary.com/r/h1%3A1626210.md
