wp-php-toolkit / corsproxy
CORSProxy component for WordPress.
Requires
- php: >=7.2
Requires (Dev)
- phpunit/phpunit: ^9.5
- dev-trunk
- v0.8.0
- v0.7.9
- v0.7.8
- v0.7.7
- v0.7.6
- v0.7.5
- v0.7.4
- v0.7.3
- v0.7.2
- v0.7.1
- v0.7.0
- v0.6.2
- v0.6.1
- v0.6.0
- v0.5.1
- v0.5.0
- v0.4.1
- v0.4.0
- v0.3.1
- v0.3.0
- v0.2.0
- v0.1.5
- v0.1.4
- v0.1.3
- v0.1.2
- v0.1.1
- v0.1.0
- 0.0.19
- 0.0.18
- 0.0.17
- 0.0.16
- 0.0.15
- v0.0.15-alpha
- 0.0.14
- 0.0.13
- 0.0.12
- 0.0.11
- v0.0.8-alpha
- 0.0.7
- v0.0.7-alpha
- 0.0.6
- v0.0.6-alpha
- v0.0.5-alpha
- v0.0.4-alpha
- v0.0.3-alpha
- v0.0.2-alpha
- v0.0.1-alpha
This package is auto-updated.
Last update: 2026-05-19 20:25:00 UTC
README
| slug | corsproxy | ||
|---|---|---|---|
| title | CORSProxy | ||
| install | wp-php-toolkit/corsproxy | ||
| see_also |
|
A small PHP CORS proxy intended for browser-side code that needs to reach servers without CORS headers.
Why this exists
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.
<?php // cors-proxy-config.php — placed next to cors-proxy.php. function playground_cors_proxy_maybe_rate_limit() { $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0'; $bucket = sys_get_temp_dir() . '/cors-rl-' . md5( $ip ); $now = time(); $window = 60; $max_req = 30; $hits = array(); if ( file_exists( $bucket ) ) { $hits = json_decode( file_get_contents( $bucket ), true ); if ( ! is_array( $hits ) ) $hits = array(); } $hits = array_filter( $hits, function ( $t ) use ( $now, $window ) { return $t > $now - $window; } ); if ( count( $hits ) >= $max_req ) { header( 'Retry-After: ' . $window ); http_response_code( 429 ); echo 'Rate limit exceeded'; exit; } $hits[] = $now; file_put_contents( $bucket, json_encode( array_values( $hits ) ) ); } echo "Config loaded — rate limiter armed.\n";
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.
<?php // cors-proxy-config.php — combine with the rate-limit example above. function playground_cors_proxy_maybe_rate_limit() { $allow = array( 'api.github.com', 'raw.githubusercontent.com', 'codeload.github.com', 'repo.packagist.org', 'downloads.wordpress.org', 'api.wordpress.org', ); $target = isset( $_SERVER['PATH_INFO'] ) ? $_SERVER['PATH_INFO'] : ( '/' . ( isset( $_SERVER['QUERY_STRING'] ) ? $_SERVER['QUERY_STRING'] : '' ) ); $target = ltrim( $target, '/' ); $host = parse_url( $target, PHP_URL_HOST ); if ( ! $host || ! in_array( strtolower( $host ), $allow, true ) ) { http_response_code( 403 ); header( 'Content-Type: text/plain' ); echo "Upstream not allowed: " . ( $host ? $host : '(none)' ); exit; } } echo "Allowlist config active.\n";
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;
}
}