Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Phar Stream Wrapper Laravel Package

typo3/phar-stream-wrapper

A PHP stream wrapper that hardens phar:// file operations by intercepting metadata deserialization, helping prevent object-injection attacks when handling untrusted archives. Useful for legacy and modern PHP security hardening.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a security shim, not a feature library. Most Laravel projects already have it in vendor/ because Composer (2.x) depends on it, so check first:

composer show typo3/phar-stream-wrapper
composer why typo3/phar-stream-wrapper

If it is present transitively, you still get no protection in your own app: Composer registers the interceptor only for its own process. Your application must register it itself.

Minimal setup — register the wrapper as early as possible in the request lifecycle, right after the autoloader:

// public/index.php
require __DIR__.'/../vendor/autoload.php';

use TYPO3\PharStreamWrapper\Manager;
use TYPO3\PharStreamWrapper\PharStreamWrapper;

Manager::initialize(
    (new Manager())->withAssertion(new \TYPO3\PharStreamWrapper\Assertion())
);

PharStreamWrapper::register();

Files to read first, in order:

  1. PharStreamWrapper — the stream wrapper itself; register(), unregister(), restore().
  2. Manager — holds the active assertion/behavior and is consulted on every phar operation.
  3. Assertion / Assertable — the policy object that decides whether a given phar:// path + command is allowed.
  4. Behavior — the behavior flag constants the assertion maps to (read, write, rename, delete, invoke).
  5. Interceptor / ConjunctionInterceptor / ExtensionInterceptor — optional pre-assertion hooks.

First real use case: an upload endpoint where a malicious file named avatar.jpg (with a forged phar manifest) plus any file_exists(), is_file(), getimagesize(), or Storage::exists() call on a user-controlled phar:// path triggers the well-known PHP metadata deserialization chain. The wrapper makes those calls fail before the phar is touched.

Implementation Patterns

Global default-deny in a service provider. The wrapper is global static state, so the cleanest Laravel integration is a dedicated provider registered in bootstrap/providers.php (or config/app.php on older versions):

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use TYPO3\PharStreamWrapper\Manager;
use TYPO3\PharStreamWrapper\PharStreamWrapper;

final class PharStreamWrapperServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        if (! config('security.phar.block_untrusted', true)) {
            return;
        }

        Manager::initialize(
            (new Manager())->withAssertion(
                new TrustedArchiveAssertion(config('security.phar.trusted', []))
            )
        );

        PharStreamWrapper::register();
    }
}

register() runs before boot() and before most of the request, which is acceptable for upload/import style threats. For maximum coverage put the two calls directly in public/index.php; anything that can trigger a phar operation before the provider runs is unprotected.

Custom Assertable for a whitelist. The built-in Assertion is pattern-based and easy to misconfigure; for a small app, an explicit allowlist is clearer and cheaper to reason about:

namespace App\Security;

use TYPO3\PharStreamWrapper\Assertable;

final class TrustedArchiveAssertion implements Assertable
{
    /** @param string[] $trustedPrefixes absolute paths, no scheme */
    public function __construct(private array $trustedPrefixes) {}

    public function assert(string $path, string $command): bool
    {
        if (! str_starts_with($path, 'phar://')) {
            return true;
        }

        foreach ($this->trustedPrefixes as $prefix) {
            if (str_starts_with($path, 'phar://'.$prefix)) {
                return true;
            }
        }

        return false;
    }
}

Config values in config/security.php:

'phar' => [
    'block_untrusted' => env('PHAR_BLOCK_UNTRUSTED', true),
    'trusted' => [
        base_path('vendor/acme/tool.phar'),
    ],
],

Use the Behavior flags when you need partial access. If you ship your own .phar and need read/invoke access to it, the idiomatic route is the built-in Assertion with behavior flags rather than a hand-rolled class. The exact set of constants (BEHAVIOR_READ, BEHAVIOR_RENAME, BEHAVIOR_DELETE, BEHAVIOR_INVOKE, …) is version-dependent — confirm against vendor/typo3/phar-stream-wrapper/src/Behavior.php in your pinned version before copying snippets from blog posts.

Unregister in tests and long-lived workers. The wrapper is process-global:

protected function tearDown(): void
{
    \TYPO3\PharStreamWrapper\PharStreamWrapper::restore();
    parent::tearDown();
}

Under Octane/Swoole, registrations survive across requests; do not flip the trust policy per request without also resetting the manager.

Layer with other defenses. This package does not sanitize paths. Keep validating URLs/paths for schemes, keep using realpath()/basename() on uploads, and keep storing uploads with non-executable extensions. The interceptor is the last line of defense.

Gotchas and Tips

  • Register before the first phar access. A wrapper registered after a malicious file_exists() is worthless. Order matters more than the policy.
  • Silent false, not always exceptions. Denied operations typically surface as false from the wrapper methods, and some paths throw TYPO3\PharStreamWrapper\Exception. If file_exists() suddenly returns false for a legitimate path, log the denied $path/$command inside your assert() before assuming a filesystem bug.
  • Extension checks are not protection. PHP identifies phars by manifest, not by .phar extension; .jpg, .zip, and extensionless files can be phars. Do not rely on ExtensionInterceptor alone.
  • unregister() vs restore(). register() replaces the native phar wrapper; unregister() removes the scheme entirely; restore() hands control back to PHP's native wrapper. Use restore() for cleanup, not unregister(), or later Phar operations in the same process may fail in confusing ways.
  • Your own phars break first. Anything that legitimately reads a bundled phar (build tooling, plugins, test fixtures) will start failing. Whitelist exact absolute prefixes, not globs over vendor/.
  • Laravel Storage bypasses nothing but also covers nothing. The wrapper only sees operations that actually go through phar://. Laravel's local/S3 drivers use fopen/file_get_contents and are unaffected unless the path itself is phar://.
  • Performance. assert() is invoked on every phar-wrapped filesystem call. Keep it to string prefix checks; avoid fnmatch, realpath, or config lookups inside it.
  • Glob and path quirks. The built-in Assertion uses pattern matching; Windows path separators, case sensitivity, and URL-encoding of the phar:// path can all change matching results. Normalize before comparing.
  • Config quirks. There is no Laravel config file shipped — anything you see in config/ is code you wrote. Publish no vendor files; wrap the package in your own provider and config key so you can toggle it in staging.
  • Debugging tip. Temporarily wrap the Assertable in a decorator that logs $path and $command and delegates to the real policy. This is the fastest way to find which legitimate operation your policy is blocking.
  • Extension points. Two are useful: a custom Assertable (policy) and a custom Interceptor (pre-check, composable with ConjunctionInterceptor). Prefer Assertable for app-level policy and Interceptor only when you need to reject paths before they reach the assertion layer.
  • Keep it updated. The last release (2024-12) tracks PHP's evolving phar behavior; the security value depends on matching the PHP version you deploy. Pin it, but do not freeze it.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels