Skip to content

Security Overview

ImageBot implements enterprise-grade security measures to protect user data, API access, and system integrity.

Password Security:

  • Passwords hashed with bcrypt (cost factor: 10)
  • Minimum password length: 8 characters
  • No password complexity requirements (passphrase approach)
  • Password reset via secure email tokens
// Password hashing (worker/index.ts)
import bcrypt from 'bcryptjs';
async function hashPassword(password: string): Promise<string> {
return await bcrypt.hash(password, 10);
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return await bcrypt.compare(password, hash);
}

Session Tokens:

  • Algorithm: HS256 (HMAC-SHA256)
  • Expiration: 7 days
  • Stored in localStorage (consider httpOnly cookies for enhanced security)
  • Automatic refresh on valid requests
// JWT generation
import jwt from '@tsndr/cloudflare-worker-jwt';
const token = await jwt.sign({
userId: user.id,
email: user.email,
exp: Math.floor(Date.now() / 1000) + (60 * 60 * 24 * 7) // 7 days
}, env.JWT_SECRET);

Security Considerations:

  • Store JWT_SECRET in Wrangler secrets (never in code)
  • Rotate secrets every 90 days
  • Implement token revocation for logout
  • Use short expiration for sensitive operations

Key Generation:

  • Format: imgbot_live_ + 32-byte random hex
  • Scoped permissions: generate, read, manage
  • Multiple keys per user
  • Individual key revocation

Storage:

  • Keys hashed with SHA-256 before storage
  • Original key shown only once
  • Database stores hash for verification
// API key validation
const apiKey = request.headers.get('Authorization')?.replace('Bearer ', '');
const keyHash = crypto.subtle.digest('SHA-256', new TextEncoder().encode(apiKey));
const validKey = await db.prepare('SELECT * FROM api_keys WHERE key_hash = ? AND is_active = 1')
.bind(keyHash).first();

User Data Separation:

  • All queries filtered by user_id
  • No cross-user data access
  • Team collaboration uses explicit permissions
  • API keys scoped to owning user
-- Example query pattern
SELECT * FROM images
WHERE user_id = ? AND is_deleted = 0
ORDER BY created_at DESC;

At Rest:

  • Cloudflare D1 encrypts all data at rest (AES-256)
  • R2 objects encrypted automatically
  • Secrets encrypted via Wrangler

In Transit:

  • All requests over HTTPS/TLS 1.2+
  • Cloudflare Universal SSL
  • HSTS enabled

PII Protection:

  • Email addresses stored in plaintext (required for auth)
  • No collection of unnecessary personal data
  • User deletion removes all associated data

Payment Data:

  • Never store credit card numbers
  • Stripe handles all payment processing
  • Only store Stripe customer IDs and payment intent IDs

Parameterized Queries: All database queries use prepared statements with bound parameters:

// Safe query
const user = await env.DB.prepare(
'SELECT * FROM users WHERE email = ?'
).bind(email).first();
// NEVER do this:
// const user = await env.DB.prepare(`SELECT * FROM users WHERE email = '${email}'`).first();

Content Security Policy:

<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'unsafe-inline' cdn.tailwindcss.com; img-src 'self' data: https:">

React Auto-Escaping:

  • React automatically escapes user content
  • Avoid dangerouslySetInnerHTML
  • Sanitize markdown with DOMPurify if needed

Token-Based:

  • All state-changing requests require authentication
  • API uses stateless JWT (no cookies)
  • SameSite cookie attribute for session cookies

Per User:

  • 100 requests/minute (standard)
  • 500 requests/minute (pro)
  • Custom limits for enterprise

Per IP:

  • 1000 requests/hour (unauthenticated)
  • Prevents brute force attacks
// Rate limit implementation
const rateLimitKey = `ratelimit:${userId}:${Math.floor(Date.now() / 60000)}`;
const count = await env.CACHE.get(rateLimitKey);
if (parseInt(count || '0') > 100) {
return new Response('Rate limit exceeded', { status: 429 });
}
await env.CACHE.put(rateLimitKey, (parseInt(count || '0') + 1).toString(), { expirationTtl: 60 });

Login Attempts:

  • Max 5 failed attempts per IP per hour
  • Exponential backoff: 2^n seconds
  • Account lockout after 10 failed attempts
  • Email notification on suspicious activity

Production Settings:

const corsHeaders = {
'Access-Control-Allow-Origin': 'https://app.yourdomain.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
};

Development:

'Access-Control-Allow-Origin': '*' // Only for local development

Wrangler Secrets:

Terminal window
wrangler secret put JWT_SECRET
wrangler secret put STRIPE_SECRET_KEY
wrangler secret put GITHUB_CLIENT_SECRET

Never commit:

  • API keys
  • Database credentials
  • OAuth client secrets
  • JWT signing keys
.env
.env.local
.env.production
.dev.vars
wrangler.toml.local
*.pem
*.key

Permission Checks:

// Verify team membership before access
async function verifyTeamMembership(env: any, userId: string, teamId: string): Promise<boolean> {
const member = await env.DB.prepare(
'SELECT * FROM team_members WHERE team_id = ? AND user_id = ? AND status = "active"'
).bind(teamId, userId).first();
return !!member;
}

Roles:

  • owner: Full control, billing access
  • admin: Manage members, settings
  • member: Create, edit content
  • viewer: Read-only access
function hasPermission(role: string, action: string): boolean {
const permissions = {
owner: ['*'],
admin: ['invite', 'manage_settings', 'edit', 'view'],
member: ['edit', 'view', 'comment'],
viewer: ['view']
};
return permissions[role]?.includes(action) || permissions[role]?.includes('*');
}
const securityHeaders = {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'geolocation=(), microphone=(), camera=()',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload'
};

Logged Events:

  • User registration
  • Login/logout
  • Password changes
  • Image generation
  • License purchases
  • Team invitations
  • API key creation/revocation
  • Payment transactions
async function logAuditEvent(env: any, event: AuditEvent) {
await env.DB.prepare(`
INSERT INTO audit_log (user_id, event_type, metadata, ip_address, timestamp)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
`).bind(event.userId, event.type, JSON.stringify(event.metadata), event.ip).run();
}

Data Retention:

  • Audit logs: 1 year
  • User data: Until account deletion
  • Deleted images: 30-day soft delete period
Terminal window
# Run security audit
npm audit
# Fix vulnerabilities
npm audit fix
# Check for outdated packages
npm outdated

Automated Tools:

  • Dependabot (GitHub) for dependency updates
  • Snyk for vulnerability scanning
  • OWASP Dependency-Check

Recommended Tests:

  1. SQL Injection - Test all input fields with SQL payloads
  2. XSS - Test with <script>alert('XSS')</script>
  3. Authentication Bypass - Test API without tokens
  4. IDOR - Test accessing other users’ resources
  5. Rate Limiting - Verify throttling works
  1. Detection

    • Monitor error rates and unusual activity
    • Alert on failed auth attempts spike
    • Review audit logs daily
  2. Containment

    • Revoke compromised API keys immediately
    • Force password reset for affected users
    • Block suspicious IP addresses
  3. Investigation

    • Review audit logs for breach scope
    • Identify attack vector
    • Assess data exposure
  4. Recovery

    • Patch vulnerability
    • Restore from backups if needed
    • Deploy security fixes
  5. Notification

    • Notify affected users within 72 hours
    • Report to authorities if required (GDPR)
    • Public disclosure if widespread

Security Issues:

User Rights:

  • Right to access data
  • Right to deletion
  • Right to data portability
  • Right to rectification

Implementation:

// Export user data
async function exportUserData(env: any, userId: string) {
const user = await getUserById(env, userId);
const images = await getImagesByUser(env, userId);
const collections = await getCollectionsByUser(env, userId);
return {
user,
images,
collections,
exported_at: new Date().toISOString()
};
}
// Delete user and all data
async function deleteUser(env: any, userId: string) {
await env.DB.batch([
env.DB.prepare('DELETE FROM images WHERE user_id = ?').bind(userId),
env.DB.prepare('DELETE FROM collections WHERE user_id = ?').bind(userId),
env.DB.prepare('DELETE FROM users WHERE id = ?').bind(userId)
]);
}
  • No card data stored in ImageBot
  • All payments processed through Stripe (PCI Level 1)
  • Use Stripe Elements for card input
  • Webhook signature verification
  1. Never commit secrets - Use Wrangler secrets
  2. Use prepared statements - Prevent SQL injection
  3. Validate all input - Trust no user input
  4. Implement rate limiting - Prevent abuse
  5. Log security events - Enable audit trail
  6. Keep dependencies updated - Patch vulnerabilities
  7. Use HTTPS everywhere - No mixed content
  1. Use strong passwords - 12+ characters, unique
  2. Enable 2FA - Additional security layer (if implemented)
  3. Review API keys - Revoke unused keys
  4. Monitor activity - Check audit logs
  5. Report suspicious activity - Contact security team

Planned Features:

  • Two-factor authentication (2FA)
  • OAuth 2.0 with GitHub/Google
  • IP whitelisting for API keys
  • Advanced threat detection
  • Automated security scanning
  • Bug bounty program
  • SOC 2 compliance