Skip to content

Security: Kritika11052005/ColdChain

Security

SECURITY.md

ColdChain — Security Document


1. Security Philosophy

ColdChain follows a defense in depth approach. Even though v1 is a local single-user tool, it is built with production security patterns from day one — because:

  1. The code will be reviewed by engineers in the interview
  2. Good security habits are non-negotiable in professional engineering
  3. The product may become multi-user in the future

Core principle: API keys never leave the backend. Period.


2. API Key Protection

Rule: Zero Keys in Frontend

All API keys live exclusively in the backend .env file. The Next.js frontend never touches them.

✅ CORRECT:
  Browser → FastAPI (no key) → External API (key added by backend)

❌ WRONG:
  Browser → External API (key in JS bundle) ← never do this

.env File Structure

# .env (never committed to git)
PROSPEO_API_KEY=pk_xxxxxxxxxxxx
BREVO_API_KEY=xkeysib-xxxxxxxxxxxx
SERPER_API_KEY=xxxxxxxxxxxx
GEMINI_API_KEY=AIzaxxxxxxxxxxxx

# Database (Turso)
TURSO_DATABASE_URL=https://your-db-name.turso.io
TURSO_AUTH_TOKEN=your_turso_auth_token

# Sender Details
SENDER_EMAIL=your_sender_email_here
SENDER_NAME=your_sender_name_here

.env.example (committed to git)

# .env.example — copy this to .env and fill in your keys
PROSPEO_API_KEY=your_prospeo_key_here
BREVO_API_KEY=your_brevo_key_here
SERPER_API_KEY=your_serper_key_here
GEMINI_API_KEY=your_gemini_key_here
TURSO_DATABASE_URL=https://your-db-name.turso.io
TURSO_AUTH_TOKEN=your_turso_auth_token
SENDER_EMAIL=your_sender_email_here
SENDER_NAME=your_sender_name_here

.gitignore (mandatory)

.env
.env.local
*.env
coldchain.db
__pycache__/
node_modules/
.next/

Validation on Startup

# FastAPI startup event
@app.on_event("startup")
async def validate_env():
    required_keys = [
        "PROSPEO_API_KEY", "BREVO_API_KEY",
        "SERPER_API_KEY", "GEMINI_API_KEY",
        "SENDER_EMAIL", "SENDER_NAME"
    ]
    missing = [k for k in required_keys if not os.getenv(k)]
    if missing:
        raise RuntimeError(f"Missing required env vars: {missing}")

3. CORS Configuration

Only allowed origins and Vercel deployments can call the FastAPI backend.

from fastapi.middleware.cors import CORSMiddleware

allowed_origins = os.getenv(
    "ALLOWED_ORIGINS",
    "http://localhost:3000,http://127.0.0.1:3000,http://localhost:3001,http://127.0.0.1:3001,https://cold-chain-xi.vercel.app"
).split(",")

app.add_middleware(
    CORSMiddleware,
    allow_origins=allowed_origins,
    allow_origin_regex=r"https://.*\.vercel\.app",
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
    max_age=3600,
)

4. Input Validation & Sanitization

Domain Input Validation

Every seed domain is validated before the pipeline runs:

import re
from pydantic import BaseModel, validator

DOMAIN_PATTERN = re.compile(
    r'^(?:[a-zA-Z0-9]'
    r'(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)'
    r'+[a-zA-Z]{2,}$'
)

class PipelineStartRequest(BaseModel):
    seed_domain: str

    @validator('seed_domain')
    def validate_domain(cls, v):
        # Strip protocol if present
        v = v.strip().lower()
        v = re.sub(r'^https?://', '', v)
        v = re.sub(r'^www\.', '', v)
        v = v.split('/')[0]  # remove path

        if not DOMAIN_PATTERN.match(v):
            raise ValueError('Invalid domain format')
        if len(v) > 253:
            raise ValueError('Domain too long')

        return v

Email Validation

from email_validator import validate_email, EmailNotValidError

def is_valid_email(email: str) -> bool:
    try:
        validate_email(email, check_deliverability=False)
        return True
    except EmailNotValidError:
        return False

SQL Injection Prevention

  • SQLAlchemy ORM used exclusively — no raw SQL strings
  • All values passed as parameters, never interpolated
  • Example:
# ✅ Safe — parameterized
result = await db.execute(
    select(Contact).where(Contact.run_id == run_id)
)

# ❌ Never do this
result = await db.execute(f"SELECT * FROM contacts WHERE run_id = '{run_id}'")

5. Rate Limiting

Per-Service Rate Limiting

Each external API call goes through a rate limiter that respects the service's published limits:

# rate_limiter.py
import asyncio
import time
from collections import defaultdict

class RateLimiter:
    def __init__(self, calls_per_second: float = 1.0):
        self.delay = 1.0 / calls_per_second
        self._last_call = defaultdict(float)
        self._locks = defaultdict(asyncio.Lock)

    async def acquire(self, service: str):
        async with self._locks[service]:
            elapsed = time.monotonic() - self._last_call[service]
            if elapsed < self.delay:
                await asyncio.sleep(self.delay - elapsed)
            self._last_call[service] = time.monotonic()

# Per-service limits
RATE_LIMITS = {
    "prospeo": RateLimiter(calls_per_second=0.5),   # 1 call per 2s
    "brevo":   RateLimiter(calls_per_second=2.0),   # 2 calls per second
    "gemini":  RateLimiter(calls_per_second=1.0),   # 1 call per second
    "serper":  RateLimiter(calls_per_second=2.0),
}

Retry with Exponential Backoff

import random

async def with_retry(func, max_retries=3, base_delay=1.0):
    for attempt in range(1, max_retries + 1):
        try:
            return await func()
        except RateLimitError:
            delay = base_delay * (2 ** (attempt - 1)) + random.uniform(0, 1)
            await asyncio.sleep(delay)
        except Exception as e:
            if attempt == max_retries:
                raise
    raise Exception("Max retries exceeded")

6. WebSocket Security

Connection Validation

WebSocket connections require a valid run_id:

@app.websocket("/ws/{run_id}")
async def websocket_pipeline(websocket: WebSocket, run_id: str):
    # Validate run_id is a real UUID
    try:
        uuid.UUID(run_id)
    except ValueError:
        await websocket.close(code=1008)  # Policy violation
        return

    # Verify run exists in DB
    run = await get_run(run_id)
    if not run:
        await websocket.close(code=1008)
        return

    await websocket.accept()
    # ... stream logs

No Sensitive Data in WebSocket Stream

Terminal output is scrubbed before sending — API keys are never logged:

import re

SENSITIVE_PATTERNS = [
    re.compile(r'(?i)(api[_-]?key|token|secret|password)\s*[:=]\s*\S+'),
    re.compile(r'xkeysib-[a-zA-Z0-9-]+'),
    re.compile(r'AIza[a-zA-Z0-9_-]{35}'),
]

def scrub_sensitive(message: str) -> str:
    for pattern in SENSITIVE_PATTERNS:
        message = pattern.sub('[REDACTED]', message)
    return message

7. Cloudflare Turnstile CAPTCHA

Add Turnstile to the pipeline input form to prevent automated abuse:

Frontend (Next.js)

import { Turnstile } from '@marsidev/react-turnstile'

// In the Input Screen component:
<Turnstile
  siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
  onSuccess={(token) => setTurnstileToken(token)}
  options={{ theme: 'dark' }}
/>

Backend Verification

async def verify_turnstile(token: str) -> bool:
    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://challenges.cloudflare.com/turnstile/v0/siteverify",
            data={
                "secret": os.getenv("TURNSTILE_SECRET_KEY"),
                "response": token,
            }
        )
    return response.json().get("success", False)

# In pipeline start endpoint:
@app.post("/api/pipeline/start")
async def start_pipeline(
    request: PipelineStartRequest,
    turnstile_token: str = Header(alias="X-Turnstile-Token")
):
    if not await verify_turnstile(turnstile_token):
        raise HTTPException(status_code=403, detail="CAPTCHA verification failed")
    # ... proceed

Additional env vars needed:

NEXT_PUBLIC_TURNSTILE_SITE_KEY=your_site_key   # public, safe in frontend
TURNSTILE_SECRET_KEY=your_secret_key            # private, backend only

Note: Turnstile is free with a Cloudflare account. Sign up at dash.cloudflare.com → Turnstile.


8. Secrets Never in Git

Pre-commit hook (optional but recommended)

Add to .git/hooks/pre-commit:

#!/bin/bash
# Block commits containing potential API keys

if git diff --cached --name-only | xargs grep -l 'AIza\|xkeysib-\|pk_\|ak_' 2>/dev/null; then
    echo "❌ Potential API key detected in staged files. Commit blocked."
    exit 1
fi

9. Security Checklist

Check Status How
API keys in .env only Never in code or frontend
.env in .gitignore First line of .gitignore
CORS restricted to localhost FastAPI CORS middleware
All inputs validated Pydantic models
SQL injection impossible SQLAlchemy ORM only
No keys in WebSocket stream scrub_sensitive() on all logs
Rate limiting per service RateLimiter class
Retry with backoff with_retry() wrapper
WebSocket run_id validated UUID check + DB lookup
CAPTCHA on input form Cloudflare Turnstile
Sensitive patterns redacted Regex scrubber on all logs

10. What Hackers Can't Do

Attack Protection
Steal API keys from JS bundle Keys never in frontend
CSRF attacks CORS + Turnstile
SQL injection via domain input Pydantic validation + ORM
Flood the pipeline endpoint Turnstile CAPTCHA + rate limiting
Read other users' run data Single-user local tool (no auth needed)
Intercept API keys in logs scrub_sensitive() on all output
Brute-force run IDs UUID v4 (122 bits of entropy)

There aren't any published security advisories