php runtime and server environment

Protocols And Wrappers

PHP stream wrappers let filesystem-style functions work with different kinds of resources. The same functions that read a local file can also read from memory streams, HTTP URLs, compressed data, and special PHP streams.

This is powerful, but it is also a common source of security mistakes. If user input can control a path or URL passed to file_get_contents(), fopen(), include, or similar functions, wrappers can change what the code actually reads.

Seeing registered wrappers

You can ask PHP which wrappers are available:

PHP example
<?php

declare(strict_types=1);

$wrappers = stream_get_wrappers();

foreach (['file', 'http', 'https', 'php', 'data'] as $name) {
    echo $name . ': ' . (in_array($name, $wrappers, true) ? 'available' : 'missing') . PHP_EOL;
}

// Prints:
// file: available
// http: available
// https: available
// php: available
// data: available

The exact list depends on extensions and PHP configuration.

Common wrappers

The file:// wrapper reads local files. Most local paths use it implicitly:

PHP example
<?php

declare(strict_types=1);

$path = __FILE__;

echo basename($path) . PHP_EOL;
echo str_starts_with(file_get_contents($path), '<?php') ? 'php file' : 'other file';
echo PHP_EOL;

// Prints:
// article-example.php
// php file

The filename in the example output is illustrative. When you run similar code from a real file, basename(__FILE__) prints that file's actual name.

The php:// wrapper exposes special streams:

PHP example
<?php

declare(strict_types=1);

$handle = fopen('php://temp', 'w+');

if ($handle === false) {
    throw new RuntimeException('Could not open temporary stream.');
}

fwrite($handle, 'temporary data');
rewind($handle);

echo stream_get_contents($handle) . PHP_EOL;

// Prints:
// temporary data

php://temp is useful for temporary data that may spill to disk after a memory threshold. php://memory stays in memory. php://input reads the raw HTTP request body in web requests, which is common for JSON APIs.

The data:// wrapper can represent inline data:

PHP example
<?php

declare(strict_types=1);

$value = file_get_contents('data://text/plain,Hello%20PHP');

if ($value === false) {
    throw new RuntimeException('Could not read data stream.');
}

echo $value . PHP_EOL;

// Prints:
// Hello PHP

Do not treat this as harmless if users can control it. A wrapper that looks like a string can still cause surprising behaviour in code that expects a normal file path.

Remote wrappers

If allow_url_fopen is enabled, functions such as file_get_contents() can read HTTP and HTTPS URLs:

PHP example
<?php

declare(strict_types=1);

echo 'allow_url_fopen: ' . ini_get('allow_url_fopen') . PHP_EOL;

// Prints:
// allow_url_fopen: 1

For serious HTTP integrations, prefer a proper HTTP client. Remote wrappers have limited ergonomics for status codes, retries, redirects, authentication, logging, testing, and structured errors.

Wrapper security

Never pass unsanitised user input directly into file or include operations:

PHP example
<?php

declare(strict_types=1);

function isAllowedLocalTemplate(string $name): bool
{
    $allowed = ['home', 'account', 'help'];

    return in_array($name, $allowed, true);
}

foreach (['home', 'php://filter/resource=index.php'] as $template) {
    echo $template . ': ' . (isAllowedLocalTemplate($template) ? 'allowed' : 'blocked') . PHP_EOL;
}

// Prints:
// home: allowed
// php://filter/resource=index.php: blocked

An allow-list is safer than trying to remove dangerous characters or protocols. This is especially important around include, require, template loading, file downloads, image processing, and import tools.

Checking a scheme

When a value is supposed to be a URL or stream URI, parse it and decide what schemes are allowed.

PHP example
<?php

declare(strict_types=1);

function hasAllowedScheme(string $uri, array $allowedSchemes): bool
{
    $scheme = parse_url($uri, PHP_URL_SCHEME);

    return is_string($scheme) && in_array(strtolower($scheme), $allowedSchemes, true);
}

echo hasAllowedScheme('https://example.com/feed.json', ['https']) ? 'allowed' : 'blocked';
echo PHP_EOL;
echo hasAllowedScheme('data://text/plain,hello', ['https']) ? 'allowed' : 'blocked';
echo PHP_EOL;

// Prints:
// allowed
// blocked

For local files, scheme checks are not enough. Resolve paths with realpath() and confirm they stay inside the directory you intended.

What you should be able to do

After this lesson, you should be able to explain what stream wrappers are, list available wrappers, use php://temp safely, recognise why allow_url_fopen matters, and avoid passing user-controlled wrapper strings into file or include operations.

Practice

Task: Allow Safe Wrapper Schemes

Create a PHP script that decides whether a stream URI is allowed for a feed importer.

The importer should only accept https:// URLs. It must reject local files, php:// streams, and data:// streams.

Requirements

  • Write a function that checks the URI scheme with parse_url().
  • Accept only the https scheme.
  • Test at least one allowed URL and three blocked values.
  • Print a clear result for each value.
  • Add a short note explaining why this matters before passing values to file_get_contents().

Check your work

The script should be runnable without making any network requests. It is only deciding whether the value would be allowed.

Show solution

One possible solution is to parse the scheme and compare it with a small allow-list.

PHP example
<?php

declare(strict_types=1);

function isAllowedFeedUri(string $uri): bool
{
    $scheme = parse_url($uri, PHP_URL_SCHEME);

    return is_string($scheme) && strtolower($scheme) === 'https';
}

$values = [
    'https://example.com/feed.json',
    'http://example.com/feed.json',
    '/var/www/private/feed.json',
    'php://filter/resource=index.php',
    'data://text/plain,hello',
];

foreach ($values as $value) {
    echo $value . ': ' . (isAllowedFeedUri($value) ? 'allowed' : 'blocked') . PHP_EOL;
}

// Prints:
// https://example.com/feed.json: allowed
// http://example.com/feed.json: blocked
// /var/www/private/feed.json: blocked
// php://filter/resource=index.php: blocked
// data://text/plain,hello: blocked

This matters because stream wrappers make some strings behave like resources. If user input goes straight into file_get_contents(), a value that looks like a path or URL may read from a wrapper the developer did not intend to support.