Skip to main content

Security Guide

Overview

This document outlines the security measures implemented in Dashtray to protect user data and prevent common vulnerabilities.

Authentication & Authorization

Session Management

  • HTTP-only cookies: Session tokens stored in HTTP-only cookies to prevent XSS attacks
  • Secure flag: Cookies marked as secure in production (HTTPS only)
  • SameSite: Cookies use SameSite=Lax to prevent CSRF attacks
  • Session expiration: Sessions expire after 30 days of inactivity
  • Token rotation: Refresh tokens rotated on each use

Password Security

  • Hashing: Passwords hashed using bcrypt (handled by Better Auth)
  • Minimum length: 8 characters required
  • Strength checking: Client-side password strength validation
  • No password storage: Passwords never stored in plain text or logs
import { checkPasswordStrength } from '$lib/utils/security';

const { score, feedback } = checkPasswordStrength(password);
// score: 0-7 (0 = weak, 7 = strong)
// feedback: Array of improvement suggestions

OAuth Security

  • State parameter: CSRF protection for OAuth flows
  • PKCE: Proof Key for Code Exchange for public clients
  • Token storage: OAuth tokens stored in HTTP-only cookies
  • Scope limitation: Minimal scopes requested

Role-Based Access Control (RBAC)

Three roles with distinct permissions:
  1. Owner
    • Full access to project
    • Manage billing and subscription
    • Delete project
    • Manage team members
  2. Editor
    • Create/edit dashboards
    • Manage integrations
    • Invite team members (viewer/editor only)
    • Cannot access billing or delete project
  3. Viewer
    • Read-only access to dashboards
    • View metrics and reports
    • Cannot modify anything
Permission checks enforced at:
  • API level (Convex functions)
  • UI level (hide/disable actions)
  • Route level (redirect unauthorized users)

Data Protection

Encryption at Rest

Sensitive data encrypted using AES-256-GCM:
  • API keys and credentials
  • OAuth tokens
  • User AI API keys
  • Webhook URLs
// Encryption implementation
import { encrypt, decrypt } from '$lib/server/encryption';

// Encrypt sensitive data
const encrypted = await encrypt(apiKey, userId); // userId used as salt

// Decrypt when needed
const decrypted = await decrypt(encrypted, userId);

Encryption in Transit

  • HTTPS only: All traffic encrypted with TLS 1.3
  • HSTS: HTTP Strict Transport Security headers
  • Certificate pinning: Cloudflare managed certificates

Key Management

  • Master key: Stored in environment variable (never in code)
  • User-specific salts: Each user’s data encrypted with unique salt
  • Key rotation: Master key can be rotated without re-encrypting all data

Input Validation

Frontend Validation

All user input validated using Zod schemas:
import { z } from 'zod';

const dashboardSchema = z.object({
  name: z.string().min(1).max(100),
  type: z.enum(['custom', 'category', 'overview']),
  category: z.enum(['payment', 'development', 'analytics']).optional()
});

// Validate input
const result = dashboardSchema.safeParse(formData);
if (!result.success) {
  // Handle validation errors
}

Backend Validation

All Convex functions validate inputs:
export const createDashboard = mutation({
  args: {
    name: v.string(),
    type: v.union(v.literal('custom'), v.literal('category')),
    category: v.optional(v.string())
  },
  handler: async (ctx, args) => {
    // Additional validation
    if (args.name.length > 100) {
      throw new Error('Name too long');
    }
    // ...
  }
});

Sanitization

User-generated content sanitized before display:
import { sanitizeInput } from '$lib/utils/security';

// Sanitize before rendering
const safe = sanitizeInput(userInput);

XSS Prevention

Content Security Policy (CSP)

Strict CSP headers configured:
// svelte.config.js
csp: {
  mode: 'auto',
  directives: {
    'default-src': ['self'],
    'script-src': ['self', 'unsafe-inline'],
    'style-src': ['self', 'unsafe-inline'],
    'img-src': ['self', 'data:', 'https:'],
    'connect-src': ['self', 'https://*.convex.cloud'],
    'frame-ancestors': ['none']
  }
}

Output Encoding

  • Svelte automatically escapes HTML in templates
  • Use {@html} only with sanitized content
  • Never use innerHTML without sanitization

Dangerous Patterns to Avoid

// ❌ BAD: Direct HTML injection
element.innerHTML = userInput;

// ✅ GOOD: Use textContent
element.textContent = userInput;

// ❌ BAD: eval() usage
eval(userCode);

// ✅ GOOD: Use safe alternatives
// (avoid eval entirely)

CSRF Protection

SameSite Cookies

Cookies use SameSite=Lax to prevent CSRF:
// Better Auth configuration
cookies: {
  sameSite: 'lax',
  secure: true,
  httpOnly: true
}

State Tokens

OAuth flows use state parameter for CSRF protection.

SQL Injection Prevention

Convex handles query parameterization automatically:
// ✅ SAFE: Convex queries are parameterized
const projects = await ctx.db
  .query('projects')
  .withIndex('by_owner', q => q.eq('ownerId', userId))
  .collect();

// ❌ NEVER: String interpolation in queries
// (Not possible with Convex, but avoid in other contexts)

Rate Limiting

API Endpoints

Rate limits enforced per project:
  • Scale/Agency: 1000 requests/hour
  • Pro: 100 requests/hour
  • Free: No API access
// Rate limiting implementation
export const apiRateLimiter = new RateLimiter(1000, 60 * 60 * 1000);

if (!apiRateLimiter.isAllowed(projectId)) {
  throw new Error('Rate limit exceeded');
}

Authentication Endpoints

  • Login: 5 attempts per 15 minutes per IP
  • Password reset: 3 attempts per hour per email
  • Email verification: 5 attempts per hour per email

Manual Sync

  • Free tier: 3 manual syncs per day
  • Pro+: Unlimited manual syncs

Webhook Security

Signature Verification

All webhooks verify signatures:
// DodoPayments webhook verification
import { verifyWebhookSignature } from 'dodopayments';

const isValid = verifyWebhookSignature(
  payload,
  signature,
  webhookSecret
);

if (!isValid) {
  throw new Error('Invalid webhook signature');
}

HTTPS Only

Webhook URLs must use HTTPS:
import { isValidWebhookUrl } from '$lib/utils/security';

if (!isValidWebhookUrl(url)) {
  throw new Error('Webhook URL must use HTTPS');
}

Idempotency

Webhooks processed idempotently using event IDs to prevent duplicate processing.

API Security

Authentication

API requests require Bearer token:
curl -H "Authorization: Bearer pp_live_..." \
  https://api.dashtray.com/v1/metrics

API Key Management

  • Keys hashed using SHA-256 before storage
  • Only last 4 characters shown in UI
  • Keys can be revoked at any time
  • Expiration dates supported
// Generate API key
const key = `pp_${env}_${generateSecureRandom(32)}`;

// Hash before storing
const keyHash = await hashString(key);

// Store hash, not key
await ctx.db.insert('apiKeys', {
  keyHash,
  keyPrefix: key.slice(0, 12), // For display
  // ...
});

Request Validation

All API requests validated:
// Validate API key format
if (!isValidApiKey(apiKey)) {
  return new Response('Invalid API key format', { status: 401 });
}

// Validate request body
const schema = z.object({
  metricId: z.string(),
  value: z.number(),
  timestamp: z.number()
});

const result = schema.safeParse(body);
if (!result.success) {
  return new Response('Invalid request', { status: 400 });
}

CORS Policy

Strict CORS policy:
// Only allow specific origins
const allowedOrigins = [
  'https://dashtray.com',
  'https://app.dashtray.com'
];

// Check origin
if (!isAllowedOrigin(origin, allowedOrigins)) {
  return new Response('Forbidden', { status: 403 });
}

// Set CORS headers
headers.set('Access-Control-Allow-Origin', origin);
headers.set('Access-Control-Allow-Credentials', 'true');

Clickjacking Prevention

X-Frame-Options

// Prevent embedding in iframes
headers.set('X-Frame-Options', 'DENY');

CSP frame-ancestors

// CSP directive
'frame-ancestors': ['none']

Security Headers

All responses include security headers:
// Security headers
headers.set('X-Content-Type-Options', 'nosniff');
headers.set('X-Frame-Options', 'DENY');
headers.set('X-XSS-Protection', '1; mode=block');
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
headers.set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');

Logging and Monitoring

Audit Logs (Scale+)

All sensitive actions logged:
  • Dashboard creation/deletion
  • Connection addition/removal
  • Team member changes
  • Subscription changes
  • API key creation/revocation
// Log audit event
await ctx.db.insert('auditLogs', {
  projectId,
  userId,
  action: 'dashboard.delete',
  resource: 'dashboard',
  resourceId: dashboardId,
  ipAddress: request.headers.get('cf-connecting-ip'),
  userAgent: request.headers.get('user-agent'),
  createdAt: Date.now()
});

Error Logging

Errors logged to Sentry:
  • Never log sensitive data (passwords, API keys)
  • Mask sensitive fields in error context
  • Include request ID for tracing

Security Monitoring

Monitor for:
  • Failed login attempts
  • Rate limit violations
  • Invalid API key usage
  • Webhook signature failures
  • Unusual access patterns

Dependency Security

Regular Updates

  • Dependencies updated monthly
  • Security patches applied immediately
  • Automated vulnerability scanning (GitHub Dependabot)

Audit

# Check for vulnerabilities
pnpm audit

# Fix vulnerabilities
pnpm audit --fix

Minimal Dependencies

  • Only essential dependencies included
  • Regular review of dependency tree
  • Prefer well-maintained packages

Secrets Management

Environment Variables

All secrets stored in environment variables:
# Never commit these
CONVEX_DEPLOY_KEY=xxx
MASTER_ENCRYPTION_KEY=xxx
DODOPAYMENTS_API_KEY=xxx
RESEND_API_KEY=xxx

.env Files

  • .env.local in .gitignore
  • .env.production.template for documentation
  • Never commit actual secrets

Rotation

Regular rotation of:
  • Master encryption key (annually)
  • API keys (as needed)
  • OAuth client secrets (as needed)

Incident Response

Security Incident Procedure

  1. Detect: Monitor logs and alerts
  2. Contain: Disable affected systems
  3. Investigate: Determine scope and impact
  4. Remediate: Fix vulnerability
  5. Notify: Inform affected users
  6. Review: Post-mortem and improvements

Contact

Report security issues to: security@dashtray.com

Security Checklist

  • All passwords hashed with bcrypt
  • Sensitive data encrypted at rest
  • HTTPS enforced everywhere
  • CSP headers configured
  • CORS policy restrictive
  • Rate limiting implemented
  • Input validation on all endpoints
  • Output encoding for user content
  • Webhook signatures verified
  • API keys hashed before storage
  • Audit logging for sensitive actions
  • Security headers on all responses
  • Dependencies regularly updated
  • No secrets in code or git
  • Error messages don’t leak info
  • Session management secure

Resources