Building a Zero-Cost Enterprise Email API: Complete Guide to Timing Attack and Header Injection Protection
A step-by-step guide to building a free, secure email notification API using Cloudflare Workers and Email Routing — with real security hardening.

黃小黃
· 10 min read
Have you ever found yourself in this situation: your project needs to send system notifications, but SendGrid charges monthly fees, AWS SES setup is complicated, and self-hosting an email server is a maintenance nightmare?
In this article, I'll share how I built a completely free email notification API using Cloudflare Workers + Email Routing. More importantly, I'll dive deep into two often-overlooked security attacks: Timing Attacks and Email Header Injection—and how to defend against them.
This isn't just theory. I've open-sourced the entire project: worker-email-notifier. Feel free to use it!
🤔 Why Build Your Own Email Notification System?
The Cost Problem with Paid Services
Let's look at the pricing of mainstream email services:
| Service | Free Tier | Beyond Free Tier |
| SendGrid | 100 emails/day (60-day trial only) | Starting at $19.95/month |
| AWS SES | 3,000 emails/month (12-month trial) | $0.10/1000 emails |
| Mailgun | 100 emails/day | Starting at $15/month |
| Postmark | 100 emails/month | Starting at $15/month |
For personal projects or small teams, these costs add up. More importantly—I just want to send a system notification. Why does it have to be this complicated?
Cloudflare's Free Tier
Cloudflare Workers + Email Routing offers:
✅ 100,000 API requests/day
✅ Generous email sending limits
✅ No credit card required
✅ Global edge network with ultra-low latency
For system notifications, monitoring alerts, and CI/CD notifications, this quota is more than enough.
Use Cases: What It's For and What It's Not
Before diving in, let's clarify what this system is designed for:
✅ Good fit:
Server monitoring alerts (high CPU, service down)
Application event notifications (new orders, payment success)
CI/CD pipeline notifications (build success/failure)
IoT device alerts
Internal team notifications
❌ Not suitable for:
Marketing emails / newsletters (Email Routing has whitelist restrictions)
User-to-user messaging
Transactional emails to arbitrary external users
Clear boundaries are important—this is a design decision, not a limitation.
🏗️ Technology Stack and Architecture Design
Why Cloudflare Workers?
| Feature | Cloudflare Workers | AWS Lambda |
| Cold start | Almost none | Can be seconds |
| Global deployment | Automatic (edge network) | Manual configuration |
| Free tier | 100,000 req/day | 1M req/month |
| Email integration | Native Email Routing | Requires SES |
| Setup complexity | Low | Medium-High |
The biggest advantage of Workers is native integration with Email Routing—no additional email service needed. Just configure DNS and you're ready to send.
System Architecture Overview
flowchart TD
A[👤 Client] -->|REST API Request| B[⚡ Cloudflare Worker]
B --> C[1. CORS Validation]
C --> D[2. API Key Check 🔒]
D -->|Timing Attack Protection| E[3. Input Validation 🔒]
E -->|Header Injection Protection| F[4. Email Sending]
F --> G[📧 Email Routing]
G -->|Recipient Whitelist| H[✅ Recipient Inbox]
Key design decisions:
Multi-platform isolation: Each platform has its own sender, API key, and recipient whitelist
Security-first: Multiple validation layers before sending any email
Flexible configuration: All settings managed via
wrangler.toml
💻 Core Implementation
Project Structure
worker-email-notifier/
├── src/
│ └── index.js # Main code (~450 lines)
├── wrangler.toml # Workers configuration
├── wrangler.toml.example # Configuration template
└── package.json
Platform Configuration (wrangler.toml)
[[send_email]]
name = "MAILER_A"
destination_address = "boss@gmail.com"
allowed_destination_addresses = ["boss@gmail.com", "admin@company.com"]
[vars.PLATFORMS.platform-a]
senderEmail = "noreply@your-domain.com"
senderName = "Platform A Notifications"
mailer = "MAILER_A"
Each platform binds to a MAILER, and each MAILER has its own whitelist—that's the key to isolation.
Email Sending Logic
import { createMimeMessage } from "mimetext";
async function sendEmail(mailer, from, fromName, to, subject, content, html) {
const msg = createMimeMessage();
msg.setSender({ name: fromName, addr: from });
msg.setRecipient(to);
msg.setSubject(subject);
// Provide both plain text and HTML versions
msg.addMessage({
contentType: "text/plain",
data: content,
});
if (html) {
msg.addMessage({
contentType: "text/html",
data: html,
});
}
const message = new EmailMessage(from, to, msg.asRaw());
await mailer.send(message);
}
Using mimetext to create MIME-compliant email format, supporting both plain text and HTML emails.
🔐 Security Protection (Part 1): Timing Attack Defense
This is one of the most important sections of this article. You may have never heard of "timing attacks," but they're a hidden killer for API security.
What is a Timing Attack?
Imagine a combination lock: every time you get a digit right, the lock makes a subtle "click" sound. A thief can listen to the sounds and guess the combination one digit at a time.
Timing attacks work exactly the same way—attackers measure server response times to deduce your API key.

Why is === Not Safe?
JavaScript's string comparison uses "short-circuit comparison":
// Assume the correct API key is "secret123"
apiKey === "secret123"
// Comparison process:
// "a" vs "s" → 1st char differs, immediately returns false (very fast)
// "s" vs "s" → same, continue comparing
// "sa" vs "se" → 2nd char differs, returns false (slightly slower)
// "se" vs "se" → same, continue...
// ...and so on
What's the problem?
First character wrong: comparison time ~0.1ms
First five characters correct: comparison time ~0.5ms
All correct: comparison time ~1ms
Attackers can:
Try "a000000..." → measure time
Try "b000000..." → measure time
Try "s000000..." → this one's slower! First char is "s"
Try "sa00000..." → measure time
...repeat until the entire API key is guessed
This is why you should never use === to compare secrets.
Constant-Time Algorithm Implementation
The solution is "constant-time comparison"—the comparison takes the same amount of time regardless of whether the strings match:
function timingSafeEqual(a, b) {
const encoder = new TextEncoder();
const aBytes = encoder.encode(a);
const bBytes = encoder.encode(b);
// Even when lengths differ, perform full comparison
// to avoid leaking length information
if (aBytes.length !== bBytes.length) {
// Compare bBytes against itself to consume constant time
// proportional to input length, then return false
let result = 1;
for (let i = 0; i < bBytes.length; i++) {
result |= aBytes[i % aBytes.length] ^ bBytes[i];
}
return false;
}
// Use XOR operation, accumulate all differences
let result = 0;
for (let i = 0; i < aBytes.length; i++) {
result |= aBytes[i] ^ bBytes[i];
}
// result is 0 only if strings are identical
return result === 0;
}
Why does this work?
XOR operation: Same = 0, different = non-zero
OR accumulation: If any bit differs, result won't be 0
Full iteration: Loop runs completely regardless of match
Constant time: Execution time depends only on string length, not content
Practical Application
function validateApiKey(providedKey, env, platformId) {
// Try to get platform-specific key
const apiKeys = parseApiKeys(env.API_KEYS);
const platformKey = apiKeys[platformId];
if (platformKey) {
// Use constant-time comparison!
return timingSafeEqual(providedKey, platformKey);
}
// Fall back to shared key
if (env.API_KEY) {
return timingSafeEqual(providedKey, env.API_KEY);
}
return false;
}
💡 Note: Cloudflare Workers now supports
crypto.subtle.timingSafeEqual()natively, and also supportscrypto.timingSafeEqual()fromnode:cryptowith thenodejs_compatflag enabled. The custom implementation above is kept for educational purposes—in production, prefer the built-in API.
🛡️ Security Protection (Part 2): Email Header Injection Defense
The second attack to defend against is "email header injection"—more common but equally overlooked.
What is Header Injection?
SMTP email structure uses \r\n to separate different headers:
From: sender@example.com\r\n
To: recipient@example.com\r\n
Subject: Hello\r\n
\r\n
Email body...
If an attacker can inject \r\n into the subject, they can insert arbitrary headers:
// Malicious input
const subject = "Hello\r\nBcc: victim1@example.com, victim2@example.com\r\n\r\nSpam content";
// Actual generated email
/*
Subject: Hello
Bcc: victim1@example.com, victim2@example.com
Spam content
*/
The attacker successfully added their own recipients!
Impact of the Attack
📧 Spam distribution: Send massive spam using your domain
🎭 Phishing: Forge the From field for phishing attacks
📛 Domain reputation damage: Your domain may be blacklisted
🔓 Data leakage: Secretly BCC sensitive information to attackers
Defense Strategies
Strategy 1: Strict Newline Detection
// Check if subject contains newline characters
if (/[\r\n]/.test(subject)) {
return jsonResponse(
{ success: false, error: "Invalid subject: contains forbidden characters" },
400
);
}
Simple but effective—reject any subject containing \r or \n.
Strategy 2: Strict Email Format Validation
function isValidEmail(email) {
// Basic length check
if (!email || email.length > 254) {
return false;
}
// Prevent consecutive dots (possible path traversal)
if (/\.\./.test(email)) {
return false;
}
// Prevent leading or trailing dots
if (email.startsWith(".") || email.endsWith(".")) {
return false;
}
// Practical email validation (RFC 5322-inspired)
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
return emailRegex.test(email);
}
This validation function:
Limits maximum length (254 characters per RFC)
Blocks consecutive dots (prevents
../style attacks)Uses strict regex validation
Strategy 3: HTML Content Escaping (XSS Prevention)
If allowing HTML emails, also prevent XSS:
function escapeHtml(text) {
const map = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
};
return text.replace(/[&<>"']/g, (char) => map[char]);
}
Error Message Sanitization
Another easily overlooked point—error messages can also leak sensitive information:
function sanitizeErrorMessage(message) {
if (typeof message !== "string") {
return "An error occurred";
}
return message
// Remove stack traces
.replace(/at\s+.*:\d+:\d+/g, "")
// Remove file paths
.replace(/\/[\w/.-]+/g, "[path]")
// Remove sensitive keywords
.replace(/password|secret|key|token/gi, "[redacted]")
.trim()
.substring(0, 200);
}
Never expose internal implementation details in error messages.
🚀 Deployment and Testing
Deployment Steps
# 1. Install dependencies
npm install
# 2. Copy and modify configuration
cp wrangler.toml.example wrangler.toml
# Edit wrangler.toml to set your domain and platforms
# 3. Login to Cloudflare
wrangler login
# 4. Generate and set API Key
npm run generate-key
wrangler secret put API_KEY
# 5. Deploy
npm run deploy
Testing the API
curl -X POST https://email-notifier.your-subdomain.workers.dev \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"platformId": "platform-a",
"to": ["boss@gmail.com"],
"subject": "🔔 System Notification",
"content": "This is a test email",
"html": "<h1>System Notification</h1><p>This is a test email</p>"
}'
Success response:
{
"success": true,
"message": "Email sent: 1 success, 0 failed",
"platform": "platform-a",
"details": [
{ "to": "boss@gmail.com", "status": "fulfilled" }
]
}
Common Issues Troubleshooting
| Issue | Possible Cause | Solution |
| 401 Unauthorized | Wrong API Key | Verify header name is X-API-Key |
| 400 Invalid platform | platformId doesn't exist | Check platform config in wrangler.toml |
| 500 Email failed | Recipient not in whitelist | Add to allowed_destination_addresses |
| CORS error | Origin not allowed | Set ALLOWED_ORIGINS environment variable |
📦 Open Source Project and Recommendations
Project Information
GitHub: supra126/worker-email-notifier
License: MIT License
Documentation: Available in English and Traditional Chinese
Free Tier Limits
| Item | Free Quota | Suitable For |
| API requests | 100,000/day | Most small-medium applications |
| Email sending | Generous daily limit | More than enough for system notifications |
Extension Suggestions
Want to extend the functionality? Here are some directions:
Add platforms: Add new
[[send_email]]andPLATFORMSconfig inwrangler.tomlEmail templates: Store HTML templates in KV Storage
Rate limiting: Integrate with Cloudflare WAF Rate Limiting rules
Logging: Use Workers Analytics or Logpush
📝 Conclusion
This article shared how to build a zero-cost email notification system using Cloudflare Workers + Email Routing, and more importantly, explored two commonly overlooked security attacks in depth.
Key Takeaways
Zero-cost doesn't mean low-quality Cloudflare's free tier is sufficient for most notification scenarios
Timing attacks are a hidden risk Never use
===to compare secrets—use constant-time algorithmsInput validation is fundamental Header injection attacks are simple but dangerous—validate all inputs strictly
Security is not optional—it's essential Even for small features, the cost of security measures is far less than post-incident remediation
Final Thoughts
The full project is open-sourced on GitHub — feel free to use it, fork it, or contribute.
References:
黃小黃
Full-stack product engineer and open source contributor based in Taiwan. I specialize in building practical solutions that solve real-world problems with focus on stability and user experience. Passionate about Product Engineering, Solutions Architecture, and Open Source collaboration.
More Posts
Beyond Chatbots: Building Real-World Stateful AI Agents on Cloudflare
Most "AI agents" you see today are just LLM wrappers with a fancy prompt. They process a request, return a response, and forget everything. No memory. No scheduling. No persistence. Real agents are different. They remember what happened yesterday. Th...
Your API Wasn't Built for AI Agents — Here's How to Fix It
By 2026, over 30% of API traffic will come from AI agents rather than human-driven applications. That number will keep climbing. Here's the uncomfortable truth: most APIs were designed for human developers who read documentation, interpret ambiguous ...
When Microservices Are Wrong: A Solutions Architect's Decision Framework
I've been that architect. The one who spun up AWS Lambda functions and ECS clusters for every new service, convinced that microservices were the only "proper" way to build modern software. After years of managing distributed complexity — and eventual...