
Research
Supply Chain Attack on Axios Pulls Malicious Dependency from npm
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.
Keep your API keys on the server where they belong!
StatEnv is a Cloudflare Worker that proxies API requests for your static websites. Instead of exposing API keys in your frontend code, the Worker keeps secrets secure and makes API calls on your behalf.
Traditional approaches expose secrets in the browser:
// INSECURE: Anyone can steal this from DevTools!
const apiKey = 'sk_live_abc123';
const data = await fetch(`https://api.example.com?key=${apiKey}`);
Even using .env files doesn't help - the secrets get bundled into JavaScript:
// Still INSECURE: Bundled into client JavaScript!
const apiKey = import.meta.env.VITE_API_KEY; // → becomes "sk_live_abc123"
StatEnv keeps secrets on the server and proxies requests:
// SECURE: API key never leaves the Worker!
const response = await fetch('https://worker.dev/myapp/weather?q=London');
const data = await response.json();
// Worker adds secret key and calls the real API
StatEnv includes an interactive CLI for easy setup and management:
# Interactive app configuration wizard
statenv add-app
# Deploy to Cloudflare
statenv deploy
# Watch real-time logs
statenv tail
# Manage secrets
statenv secrets list
statenv secrets add
# Run tests
statenv test
See cli/README.md for complete CLI documentation.
Edit src/index.js to define which APIs your apps can access:
const APP_CONFIG = {
myblog: {
origins: ['https://myblog.com', 'http://localhost:3000'],
apis: {
weather: {
url: 'https://api.weatherapi.com/v1/current.json',
secret: 'MYBLOG_WEATHER_KEY',
method: 'GET',
params: ['q'], // Allowed query params
cache: 300, // Cache for 5 minutes
},
analytics: {
url: 'https://api.example.com/track',
secret: 'MYBLOG_ANALYTICS_KEY',
method: 'POST',
bodyFields: ['event', 'data'], // Allowed body fields
},
},
},
};
# Login to Cloudflare
wrangler login
# Set your secrets (never in code!)
wrangler secret put MYBLOG_WEATHER_KEY
wrangler secret put MYBLOG_ANALYTICS_KEY
# Deploy
wrangler deploy
Just use plain fetch - no library needed!
<script>
const WORKER_URL = 'https://statenv.yourname.workers.dev';
const APP_NAME = 'myblog';
// GET request
const response = await fetch(`${WORKER_URL}/${APP_NAME}/weather?q=London`);
const weather = await response.json();
console.log(weather.current.temp_c);
// POST request
await fetch(`${WORKER_URL}/${APP_NAME}/analytics`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'page_view',
data: { page: '/home' }
})
});
</script>
Browser → External API (with exposed secret) ❌
Browser → StatEnv Worker → External API (with secret) ✅
env.get('weather', { q: 'London' })MYBLOG_WEATHER_KEY from secure storageapi.weatherapi.com?key=SECRET&q=London| Feature | .env (Bundled) | StatEnv Proxy |
|---|---|---|
| Secrets in browser | ❌ Yes (bundled) | ✅ No (server-only) |
| Extractable by users | ❌ Yes | ✅ No |
| Rotate without rebuild | ❌ No | ✅ Yes |
| Origin validation | ❌ No | ✅ Yes |
| Rate limiting | ❌ No | ✅ Yes (built-in) |
| Edge caching | ❌ No | ✅ Yes (global) |
| Actually secure | ❌ No | ✅ Yes |
const APP_CONFIG = {
appName: {
origins: ['https://yourdomain.com'], // Allowed domains
apis: {
apiName: {
url: 'https://api.external.com/endpoint', // Real API URL
secret: 'APPNAME_SECRETNAME', // Wrangler secret name
method: 'GET', // or 'POST'
params: ['param1', 'param2'], // Allowed query params (GET)
bodyFields: ['field1', 'field2'], // Allowed body fields (POST)
cache: 300, // Cache seconds (optional)
},
},
},
};
Secrets follow the pattern: {APPNAME}_{SECRETNAME}
# For app "myblog" and secret "WEATHER_KEY"
wrangler secret put MYBLOG_WEATHER_KEY
# For app "myshop" and secret "STRIPE_KEY"
wrangler secret put MYSHOP_STRIPE_KEY
apis: {
weather: {
url: 'https://api.weatherapi.com/v1/current.json',
secret: 'MYBLOG_WEATHER_KEY',
method: 'GET',
params: ['q', 'lang'], // Only these params are forwarded
cache: 300 // Cache responses for 5 minutes
}
}
Client usage:
await env.get('weather', { q: 'London', lang: 'en' });
// Worker calls: api.weatherapi.com?key=SECRET&q=London&lang=en
apis: {
analytics: {
url: 'https://api.analytics.com/track',
secret: 'MYBLOG_ANALYTICS_KEY',
method: 'POST',
bodyFields: ['event', 'data', 'timestamp'] // Only these fields allowed
}
}
Client usage:
await env.post('analytics', {
event: 'button_click',
data: { button: 'signup' },
timestamp: Date.now(),
});
StaticEnv/
├── README.md ← You are here
├── LICENSE ← Apache 2.0
├── wrangler.toml ← Worker configuration
├── package.json ← npm scripts
│
├── src/
│ └── index.js ← Worker proxy code
│
├── docs/
│ ├── MONITORING.md ← Complete monitoring guide
│ └── MONITORING_QUICK_REF.md ← Quick reference
│
└── tests/
├── README.md ← Test documentation
└── worker.test.js ← Integration tests
# Watch all requests in real-time
wrangler tail
# Monitor errors only
wrangler tail --status error
# Monitor cache performance
wrangler tail --search "Cache"
# Monitor rate limiting
wrangler tail --search "Rate limit"
See docs/MONITORING.md for complete guide or docs/MONITORING_QUICK_REF.md for quick reference.
View detailed analytics in Cloudflare Dashboard:
# Install dependencies
pnpm install
# Run all tests
pnpm test
# Watch mode (for development)
pnpm run test:watch
See tests/README.md for details.
pnpm install
# or npm install
wrangler login
Edit src/index.js and define your apps and API endpoints.
wrangler secret put APPNAME_SECRETNAME
# Enter the secret value when prompted
pnpm run deploy
# or npm run deploy
const WORKER_URL = 'https://statenv.me.workers.dev';
const APP_NAME = 'myblog';
// Get weather for London
const response = await fetch(`${WORKER_URL}/${APP_NAME}/weather?q=London`);
const weather = await response.json();
console.log(`${weather.location.name}: ${weather.current.temp_c}°C`);
// Track page views
await fetch(`${WORKER_URL}/${APP_NAME}/analytics`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'page_view',
data: {
page: window.location.pathname,
timestamp: Date.now(),
},
}),
});
// Call different endpoints
const weather = await fetch(`${WORKER_URL}/${APP_NAME}/weather?q=London`).then((r) => r.json());
const forecast = await fetch(`${WORKER_URL}/${APP_NAME}/forecast?q=London&days=3`).then((r) =>
r.json()
);
await fetch(`${WORKER_URL}/${APP_NAME}/analytics`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event: 'api_call' }),
});
Responses are cached at the edge using Cloudflare's Cache API:
apis: {
weather: {
url: 'https://api.weatherapi.com/v1/current.json',
cache: 300 // Cache for 5 minutes (300 seconds)
}
}
How it works:
Benefits:
Cache headers:
X-Cache: HIT (from cache)
X-Cache: MISS (fetched from API)
Disable caching:
apis: {
realtime: {
url: 'https://api.example.com/live',
cache: 0 // or omit cache property
}
}
Built-in rate limiting prevents abuse and protects your API quotas:
// Configure in src/index.js
const RATE_LIMIT = {
maxRequests: 100, // Maximum requests per window
windowMs: 60000, // Time window (1 minute)
perIP: true, // Rate limit per IP (recommended)
perApp: false, // Or rate limit per app
};
Features:
X-RateLimit-* headersResponse headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1704126000000
Retry-After: 45 (only on 429 errors)
Only requests from whitelisted domains are allowed:
myblog: {
origins: [
'https://myblog.com',
'https://www.myblog.com',
'http://localhost:3000', // For development
];
}
Requests from other domains get a 403 Forbidden response.
Only specified parameters are forwarded:
params: ['q', 'lang']; // Other params are ignored
Prevents clients from injecting malicious parameters.
Only allowed fields in POST bodies:
bodyFields: ['event', 'data']; // Other fields stripped
Protects against injection attacks.
Each app/API can use different secrets. If one is compromised, others remain safe.
Q: Is this more secure than .env files?
A: Yes! With .env, secrets are bundled into JavaScript where users can extract them. With StatEnv, secrets never leave the Worker.
Q: Does this add latency?
A: First request adds ~20-50ms for the proxy hop. But with caching enabled, subsequent requests are served from Cloudflare's edge cache and are actually faster than calling the API directly!
Q: Can users still abuse my APIs?
A: You can add rate limiting in the Worker (per origin, per IP, etc.). Much harder to abuse than exposed keys.
Q: What if my Worker goes down?
A: Cloudflare Workers have 99.99%+ uptime. Much more reliable than exposing keys that can be stolen forever.
Q: How many API calls can I make?
A: Cloudflare free tier allows 100,000 requests/day. Plenty for most use cases.
Q: Can I use this with any API?
A: Yes! Just configure the endpoint URL, method, and parameters in the Worker config.
Q: What about API rate limits?
A: The built-in caching dramatically reduces external API calls. If 1000 users request weather data within 5 minutes, the external API is only called once!
Q: How much does caching save?
A: Example: Weather API costs $0.0001 per request. Without caching, 10,000 requests = $1. With 5-minute caching, maybe 100 requests = $0.01. 99% cost reduction!
Check that your domain is in the origins array:
origins: ['https://yourdomain.com', 'http://localhost:3000'];
The Wrangler secret is missing. Set it:
wrangler secret put APPNAME_SECRETNAME
The Worker handles CORS automatically. Make sure you're making requests from an allowed origin.
Check that the API name matches your config:
apis: {
weather: { ... } // Call with fetch('/myblog/weather?...')
}
You've hit the rate limit. Check the response headers:
const response = await fetch(url);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
console.log(`Rate limited. Retry in ${retryAfter} seconds`);
}
To adjust limits, edit RATE_LIMIT in src/index.js:
const RATE_LIMIT = {
maxRequests: 200, // Increase limit
windowMs: 60000, // Keep 1 minute window
};
src/index.js and configure your appswrangler secret putwrangler deployfetch calls to your WorkerKeep your secrets secret! 🔐
Ready to deploy? Run wrangler deploy
FAQs
Secure Cloudflare Worker API proxy for your apps
We found that statenv demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.

Security News
TeamPCP is partnering with ransomware group Vect to turn open source supply chain attacks on tools like Trivy and LiteLLM into large-scale ransomware operations.