{"record":{"id":"f14b08f06ac23e2b","repo":"PHPOffice/PhpSpreadsheet","slug":"disallowed-stream-wrapper-used-for-filename","errorCode":null,"errorMessage":"Disallowed stream wrapper used for {$filename}","messagePattern":"Disallowed stream wrapper used for (.+?)","errorType":"exception","errorClass":"PhpOffice\\PhpSpreadsheet\\Exception","httpStatus":null,"severity":"error","filePath":"src/PhpSpreadsheet/Shared/File.php","lineNumber":152,"sourceCode":"        return tempnam(self::sysGetTempDir(), 'phpspreadsheet') ?: throw new Exception('Could not create temporary file');\n    }\n\n    /**\n     * Blocks phar:// and similar RCE-bearing wrappers.\n     * Note that many protocols, including http and zip, will already\n     * return false for is_file.\n     * A whitelist of protocols may be added if needed in future.\n     * data: is intentionally allowed (see #4823); callers needing strict\n     * on-disk-only semantics must validate $filename themselves.\n     */\n    public static function prohibitWrappers(string $filename): void\n    {\n        if (\n            Preg::IsMatch('~^phar://~i', $filename)\n            || (Preg::isMatch('/^([\\w.\\s\\x00-\\x1f]+):/', $filename) && !Preg::isMatch('/^([\\w.]+):/', $filename))\n            || Preg::isMatch('~^[\\w.]+://.*phar:~is', $filename)\n        ) {\n            throw new Exception(\n                \"Disallowed stream wrapper used for {$filename}\"\n            );\n        }\n    }\n\n    /**\n     * Assert that given path is an existing file and is readable, otherwise throw exception.\n     */\n    public static function assertFile(string $filename, string $zipMember = ''): void\n    {\n        self::prohibitWrappers($filename);\n        if (!is_file($filename) || !is_readable($filename)) {\n            throw new ReaderException('File \"' . $filename . '\" does not exist or is not readable.');\n        }\n\n        if ($zipMember !== '') {\n            $zipfile = \"zip://$filename#$zipMember\";\n            if (!self::fileExists($zipfile)) {","sourceCodeStart":134,"sourceCodeEnd":170,"githubUrl":"https://github.com/PHPOffice/PhpSpreadsheet/blob/65b080eef4d9fd11a5796135ab145883e5c3d6a6/src/PhpSpreadsheet/Shared/File.php#L134-L170","documentation":"File::prohibitWrappers() blocks RCE-bearing PHP stream wrappers before any file operation (src/PhpSpreadsheet/Shared/File.php:152): literal phar:// prefixes, wrapper-looking prefixes containing whitespace/control characters, and any wrapper chain embedding phar: (e.g. compress.zlib://phar://evil.phar/x). assertFile() and testFileNoThrow() call it for every filename, so all readers reject such paths outright; data:// is intentionally allowed.","triggerScenarios":"Passing 'phar://archive.phar/sheet.xlsx' to a reader's load(); smuggling phar through a wrapper chain (compress.zlib://phar://...); user-supplied filenames reused as read paths that happen to contain a colon-prefixed wrapper pattern with odd characters.","commonSituations":"Security-hardened apps after the 2018 phar deserialization CVE wave; request handlers that concatenate upload metadata into paths; test suites exercising wrapper paths; attempting to load remote/protocol URLs the reader never supported.","solutions":["Copy stream content to a real temp file first (File::temporaryFilename() + file_put_contents()), then load that path","Reject or sanitize any input containing '://' at your trust boundary before it reaches the library","Do not try to bypass the check — it is a deliberate remote-code-execution guard, not a bug"],"exampleFix":"// before\n$spreadsheet = \\PhpOffice\\PhpSpreadsheet\\IOFactory::load('phar://archive.phar/sheet.xlsx'); // throws\n\n// after\n$data = file_get_contents('phar://archive.phar/sheet.xlsx'); // explicit, audited stream use\n$tmp = \\PhpOffice\\PhpSpreadsheet\\Shared\\File::temporaryFilename();\nfile_put_contents($tmp, $data);\n$spreadsheet = \\PhpOffice\\PhpSpreadsheet\\IOFactory::load($tmp);","handlingStrategy":"validation","validationCode":"function isSafeLocalPath(string $path): bool\n{\n    if (preg_match('~^[\\w.]+://~i', $path)) {\n        return false; // any stream wrapper, incl. phar://\n    }\n\n    return true;\n}\n\nif (!isSafeLocalPath($userPath)) {\n    throw new InvalidArgumentException('Stream wrappers are not accepted');\n}","typeGuard":null,"tryCatchPattern":"try {\n    $spreadsheet = \\PhpOffice\\PhpSpreadsheet\\IOFactory::load($path);\n} catch (\\PhpOffice\\PhpSpreadsheet\\Reader\\Exception $e) {\n    if (str_contains($e->getMessage(), 'Disallowed stream wrapper')) {\n        // attacker-controlled or misused path: log & reject, do not retry\n    }\n}","preventionTips":["Reject filenames containing '://' at the upload boundary","Persist user/stream content to a temp file, then load the path","Never feed phar:// or chained wrappers to file-based libraries"],"tags":["security","stream-wrappers","phar","file-input","rce-guard"],"backgroundTag":"unsafe-path-rejected","analyzedSha":"65b080eef4d9fd11a5796135ab145883e5c3d6a6","analyzedAt":"2026-08-17T05:40:41.646Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}