PHP Toolkit

CORSProxy

A small PHP CORS proxy intended for browser-side code that needs to reach servers without CORS headers.

composer require wp-php-toolkit/corsproxy

A Playground-style browser tool reads https://api.github.com/repos/WordPress/php-toolkit, a plugin ZIP from downloads.wordpress.org, or a raw fixture from GitHub. The browser blocks the response when the upstream server does not send the required CORS headers, even though PHP can fetch the same public URL server-side.

The CORSProxy component is that server-side bridge. It accepts a target URL, fetches it from PHP, and returns a browser-readable response. Because an open proxy is a security and abuse risk, real deployments should add host allowlists, rate limits, header controls, and private-network protections appropriate to their environment.

Run the proxy locally

Run on your machine: the proxy needs to listen on a port. Start PHP's built-in server and request any HTTPS URL through it.

PLAYGROUND_CORS_PROXY_DISABLE_RATE_LIMIT=1 \
  php -S 127.0.0.1:5263 vendor/wp-php-toolkit/corsproxy/cors-proxy.php

# In another terminal:
curl -s "http://127.0.0.1:5263/cors-proxy.php/https://api.github.com/repos/WordPress/php-toolkit" | head

Production rate limiting

Drop a cors-proxy-config.php next to cors-proxy.php. If that file defines a playground_cors_proxy_maybe_rate_limit() function, the proxy calls it before forwarding any request — your one chance to reject early. Without that function the proxy relies only on its coarse built-in safeguards, so production deployments should provide their own rate limiting.

This example uses a per-IP token bucket stored on disk. Replace with Redis or memcached for multi-host deployments.

Allowlist upstream hosts

Out of the box the proxy will fetch any public URL. Most real deployments want a fixed list of upstreams — GitHub, Packagist, wp.org. Both the rate-limit logic and the allowlist live in the same hook, since cors-proxy.php only calls playground_cors_proxy_maybe_rate_limit() once. The example below shows just the allowlist concern; in practice you stack both in one function inside cors-proxy-config.php.

Browser-side fetch through the proxy

Once deployed, the client side is just fetch() with the proxy URL. Drop this into any HTML page.

const PROXY = "https://cors.example.com/cors-proxy.php";

async function viaProxy(url, init = {}) {
  const res = await fetch(`${PROXY}/${url}`, {
    ...init,
    headers: {
      ...(init.headers || {}),
      "X-Cors-Proxy-Allowed-Request-Headers": "Authorization",
    },
  });
  if (!res.ok) throw new Error(`Proxy returned ${res.status}`);
  return res;
}

const repo = await viaProxy("https://api.github.com/repos/WordPress/php-toolkit").then(r => r.json());
console.log(repo.full_name, repo.stargazers_count);

Deploy behind nginx

The proxy is a single PHP script — any SAPI works. nginx + php-fpm is a common production setup. PATH_INFO is what the proxy reads to learn the target URL.

server {
  listen 443 ssl http2;
  server_name cors.example.com;

  root /var/www/cors-proxy;
  index cors-proxy.php;

  location ~ ^/cors-proxy\.php(/.*)?$ {
    fastcgi_pass unix:/run/php/php8.1-fpm.sock;
    fastcgi_split_path_info ^(.+\.php)(/.*)$;
    fastcgi_param SCRIPT_FILENAME $document_root/cors-proxy.php;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    include fastcgi_params;
  }
}

See also