Current Release: Version 2.6.0
Current Release: Version 2.6.0
Release Date: September 2, 2026
Status: Stable Release
📈 Version 2.6.0 - Security Layer (Rate Limit + Headers + HMAC)
Released: September 2, 2026
🌟 Highlights
- Defence-in-depth security layer for the entire stack —
rate limiting, response-header hardening, abuse detection,
optional API-key + HMAC signing — all implemented in pure
PHP, no Composer / Redis / Memcached required - Sliding-window rate limiter with per-IP and per-API-key
bucketing, auto-ban on abuse, and millisecond resolution state
persisted in tiny JSON files (storage/ratelimit/) - Nginx-level rate limits + connection caps + buffer
hardening + an optional HTTPS server block template - Backwards-compatible — every existing endpoint continues
to work; security features default to safe values
🌟 New Features
🚦 RateLimiter.php — Sliding-Window Rate Limiter (file-backed)
api/includes/security/RateLimiter.php
- Sliding-window algorithm with millisecond resolution
- Per-bucket state files (SHA-1 of
(route, identity)) at
storage/ratelimit/bucket__<sha1>.json - Atomic writes via
flock(LOCK_EX)+ temp file +rename() - Per-IP bucket by default; per-API-key bucket when the caller
sends anX-API-Keyheader - Auto-ban when a bucket accumulates too many failures
(SECURITY_FAIL_LIMIT/SECURITY_BAN_WINDOWenv vars,
default 10 failures → 5 minute ban) - HTTP 429 response with:
X-RateLimit-Limit/X-RateLimit-Remaining/X-RateLimit-ResetRetry-After(seconds)- JSON body
{"success":false,"error":"Too many requests",...}
- Optional global bucket (
RATELIMIT_GLOBAL_LIMIT/
RATELIMIT_GLOBAL_WINDOW) that counts every hit across all
routes for a single identity - Garbage-collection helper (
RateLimiter::gc()) for stale
buckets — wired into a cron-friendly command line entry point
🛡️ Security.php — Defensive Headers, Input Sanitisation, HMAC
api/includes/security/Security.php
Security::sendHeaders()emits a full suite of defensive
response headers idempotently:X-Content-Type-Options: nosniffX-Frame-Options: SAMEORIGINX-XSS-Protection: 1; mode=blockReferrer-Policy: strict-origin-when-cross-originPermissions-Policy: geolocation=(), microphone=(), camera=(), payment=()Strict-Transport-Security(only emitted when TLS is
configured)
Security::safeString()/safeInt()/safeEnum()— typed
coercion with bounds, regex whitelist, and null-byte strippingSecurity::containsMalicious()— pattern probe for SQL
injection, path traversal, XSS, shell injection and PHP
injection probesSecurity::enforceJson()— caps JSON body size (default
64 KiB) and nesting depth (default 8)Security::generateApiKey()— cryptographically secure API
key generatorSecurity::hmac()/verifyHmac()— constant-time signature
comparison viahash_equalsSecurity::clientFingerprint()— SHA-1 of IP + UA + Accept +
Accept-Language (useful for forensics / additional rate
buckets)
🪝 Bootstrap Helpers
New helpers wired into every endpoint:
api_security_init()— reads.env, configures the
RateLimiteronce per request (static-cached)api_rate_limit($bucket, $policy)— the main entry point
called by every endpoint right after the preflightapi_rate_limit_fail($bucket)— explicit failure counter
(e.g. for failed signature verification)api_safe_json_body()— JSON body validator that returns
nullon overflow / invalid depthapi_verify_signature()— HMAC verifier with optional
algorithm choice (sha256/sha512)api_unauthorized($reason)— emits a 401 JSON response and
exits cleanly
⚙️ Per-Tool Rate-Limit Policies
| Endpoint | Limit | Window |
|---|---|---|
randomizer, fortune-teller |
120 / min | 60 s |
password-generator, username-generator, health-calculator |
60 / min | 60 s |
qr-code-generator, promptpay-qr-generator |
30 / min | 60 s |
Override any of them via RATELIMIT_DEFAULT_LIMIT /
RATELIMIT_DEFAULT_WINDOW env vars, or edit
api_config.php directly. Heavier endpoints (QR generators)
get a tighter budget because they call out to goQR.me or do
heavy image rendering.
🌐 Nginx Hardening
limit_conn_zone+limit_req_zonemoved to the top-level
http {}context (the only place nginx accepts them)/api/location now applies:limit_req zone=myapis_req burst=60 nodelay— request
rate cap with a 60-request burstlimit_conn myapis_conn 20— concurrent connection cap
client_max_body_size,client_header_buffer_size,
large_client_header_buffers,client_body_timeout,
client_header_timeout— buffer hardening against
slowloris / large-header DoS- Direct access to
api_config.phpis now blocked - Commented-out HTTPS server block template with HSTS —
ready to uncomment when TLS certificates are mounted
🐳 Docker / Storage Provisioning
docker/entrypoint.shnow provisions
/var/www/myapis-storage/ratelimit/and
/var/www/myapis-storage/logs/with correct ownership
(www-data:www-data,0775)docker-compose.ymladds two persistent named volumes:myapis-ratelimit→ rate-limit state filesmyapis-logs→ application logs
- All security env vars are forwarded to the
phpservice
(SECURITY_ENABLED,RATELIMIT_DEFAULT_LIMIT,
RATELIMIT_DEFAULT_WINDOW,RATELIMIT_GLOBAL_LIMIT,
RATELIMIT_GLOBAL_WINDOW,SECURITY_FAIL_LIMIT,
SECURITY_BAN_WINDOW,SECURITY_BLACKLIST,
SECURITY_WHITELIST,TRUST_CF_CONNECTING_IP,
TRUST_X_FORWARDED_FOR,TRUSTED_PROXIES,
RATELIMIT_STORAGE_DIR,MYAPIS_LOG_DIR)
📝 Configuration Surface (example.env)
New documented variables (all optional, all safe-by-default):
SECURITY_ENABLED— global kill-switch (defaulttrue)RATELIMIT_DEFAULT_LIMIT/RATELIMIT_DEFAULT_WINDOW
— defaults for every endpoint (60 / 60 s)RATELIMIT_GLOBAL_LIMIT/RATELIMIT_GLOBAL_WINDOW
— global abuse cap (e.g. 1000 / 60 s)SECURITY_FAIL_LIMIT/SECURITY_BAN_WINDOW— auto-ban
thresholds (10 failures → 300 s ban)SECURITY_BLACKLIST/SECURITY_WHITELIST— comma-
separated CIDR / IP listsTRUST_CF_CONNECTING_IP— readCF-Connecting-IP
(Cloudflare)TRUST_X_FORWARDED_FOR— readX-Forwarded-Forwhen
the request comes from a trusted proxyTRUSTED_PROXIES— CIDR allowlist for proxy trustRATELIMIT_STORAGE_DIR/MYAPIS_LOG_DIR— runtime
paths
🔒 Optional HMAC Request Signing
if (!api_verify_signature('YOUR_SHARED_SECRET', 'sha256', true)) {
api_unauthorized('Invalid signature');
}Clients send the hex digest of the request body in the
X-Signature header. Constant-time comparison via
hash_equals() prevents timing attacks.
🌍 Reverse-Proxy / Cloudflare Awareness
When the stack sits behind Cloudflare or another trusted proxy,
the rate limiter buckets by the real client IP instead of the
proxy IP:
TRUST_CF_CONNECTING_IP=true
# or
TRUST_X_FORWARDED_FOR=true
TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12Do not enable these flags without listing your proxy IPs
in TRUSTED_PROXIES — otherwise clients can spoof their IP
via the X-Forwarded-For header.
📚 Documentation
- New 🔒 Security section in
README.md— covers rate limiting, headers,
input validation, nginx hardening, HMAC, whitelist /
blacklist, reverse-proxy trust, and the global kill switch - README Latest Updates bumped to v2.6.0
RELEASE.md— this entry
📁 New / Updated Files
New files
api/includes/security/RateLimiter.php— sliding-window
limiterapi/includes/security/Security.php— defensive headers,
input helpers, HMACapi/includes/api_config.php— per-tool rate-limit policiesstorage/ratelimit/— runtime bucket state (git-ignored)storage/logs/— runtime app logs (git-ignored)
Updated files
api/includes/bootstrap.php— security init + new helpersapi/password-generator/index.php— addsapi_rate_limit()api/username-generator/index.php— addsapi_rate_limit()api/randomizer/index.php— addsapi_rate_limit()api/fortune-teller/index.php— addsapi_rate_limit()api/health-calculator/index.php— addsapi_rate_limit()api/qr-code-generator/index.php— addsapi_rate_limit()api/promptpay-qr-generator/index.php— adds
api_rate_limit()docker/nginx/default.conf— rate limits, connection caps,
buffer hardening, blockedapi_config.php, HTTPS templatedocker/entrypoint.sh— provisionratelimit/andlogs/docker-compose.yml— persistent volumes + security env vars.gitignore— excludestorage/example.env— full security configuration documentationREADME.md— new 🔒 Security section, Latest Updates bump
✅ Verification
php -lpasses for every modified PHP file (all 7 endpoints,
bootstrap.php,api_config.php, bothsecurity/*.php
files)- Live HTTP smoke tests against the running stack:
- Every API endpoint returns
X-RateLimit-*and
Retry-Afterheaders Permissions-Policy,X-Frame-Options, etc. are present
in the response- Request #119 of
/api/randomizer/returns
HTTP 429 withRetry-After: 47,X-RateLimit-Remaining: 0
and the expected JSON body - Public pages (
/,/tools/randomizer.php,
/api-specs/randomizer.php) return 200 OK (rate
limiting is scoped to/api/) - Storage verified — bucket JSON files appear under
/var/www/myapis-storage/ratelimit/withwww-dataownership
- Every API endpoint returns
docker compose configvalidates cleanly (no YAML errors,
no duplicate keys)- Nginx config validates cleanly:
- No "directive is duplicate" errors
- No "directive is not allowed here" errors
limit_req/limit_conncorrectly applied at the/api/
location