Security

Security Best Practices

Security guidelines for building secure applications with the Cloudillo API.

Authentication Security

Token Storage

Never store tokens insecurely:

// ❌ NEVER - tokens in cookies without httpOnly
document.cookie = `token=${token}`

// ❌ NEVER - tokens in URL
window.location.href = `/app?token=${token}`

// ❌ NEVER - tokens in localStorage on shared devices
// (acceptable for user-owned devices with understanding of risks)
localStorage.setItem('token', token)

// ✅ GOOD - sessionStorage for temporary sessions
sessionStorage.setItem('token', token)

// ✅ BETTER - in-memory storage only
class SecureTokenStore {
  private token: string | null = null

  setToken(token: string) {
    this.token = token
  }

  getToken(): string | null {
    return this.token
  }

  clearToken() {
    this.token = null
  }
}

// ✅ BEST - httpOnly secure cookies (set by server)
// Server sets: Set-Cookie: token=...; HttpOnly; Secure; SameSite=Strict

Token Transmission

Always use HTTPS in production:

// ❌ NEVER in production - unencrypted HTTP
const API_URL = 'http://api.example.com'

// ✅ ALWAYS in production - encrypted HTTPS
const API_URL = 'https://api.example.com'

// ✅ GOOD - enforce HTTPS
function enforceHttps() {
  if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
    location.replace(`https:${location.href.substring(location.protocol.length)}`)
  }
}

Properly set Authorization header:

// ✅ Correct Authorization header format
fetch('/api/action', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
})

// ❌ NEVER log or expose tokens
console.log('Token:', token)  // NO!

// ✅ Safe logging for debugging
console.log('Token present:', !!token)
console.log('Token length:', token?.length)

Token Validation

Validate tokens before use:

function isTokenValid(token: string): boolean {
  if (!token) return false

  try {
    // Parse JWT
    const parts = token.split('.')
    if (parts.length !== 3) return false

    // Decode payload
    const payload = JSON.parse(atob(parts[1]))

    // Check expiration
    const now = Math.floor(Date.now() / 1000)
    if (payload.exp && payload.exp < now) {
      return false
    }

    // Check issuer (if known)
    if (payload.iss && !isValidIssuer(payload.iss)) {
      return false
    }

    return true
  } catch (err) {
    return false
  }
}

// Use before making requests
if (!isTokenValid(token)) {
  // Redirect to login or refresh token
  window.location.href = '/login'
}

Token Refresh

Implement secure token refresh:

class SecureAuthManager {
  private refreshInProgress: Promise<string> | null = null

  async getValidToken(): Promise<string> {
    const currentToken = this.getToken()

    if (this.isExpiringSoon(currentToken)) {
      // Prevent concurrent refresh attempts
      if (!this.refreshInProgress) {
        this.refreshInProgress = this.refreshToken(currentToken)
          .finally(() => {
            this.refreshInProgress = null
          })
      }
      return this.refreshInProgress
    }

    return currentToken
  }

  private async refreshToken(oldToken: string): Promise<string> {
    try {
      const response = await fetch('/api/auth/login-token', {
        headers: { 'Authorization': `Bearer ${oldToken}` }
      })

      if (!response.ok) {
        throw new Error('Token refresh failed')
      }

      const { data } = await response.json()
      this.setToken(data.token)
      return data.token
    } catch (err) {
      // Clear invalid token
      this.clearToken()

      // Redirect to login
      window.location.href = '/login'

      throw err
    }
  }

  private isExpiringSoon(token: string): boolean {
    try {
      const payload = JSON.parse(atob(token.split('.')[1]))
      const expiresAt = payload.exp * 1000
      const now = Date.now()
      const fiveMinutes = 5 * 60 * 1000

      return expiresAt - now < fiveMinutes
    } catch {
      return true  // Treat invalid tokens as expired
    }
  }
}

Input Validation and Sanitization

Validate All User Input

Client-side validation (for UX):

function validateActionInput(input: any): ValidationResult {
  const errors: string[] = []

  // Check required fields
  if (!input.type) {
    errors.push('Action type is required')
  }

  // Validate action type
  const validTypes = ['POST', 'COMMENT', 'REACT', 'CONNECT', 'FOLLOW',
                      'CONVERSATION', 'ACKNOWLEDGE', 'FILESHARE', 'STATISTICS']
  if (input.type && !validTypes.includes(input.type)) {
    errors.push(`Invalid action type: ${input.type}`)
  }

  // Validate content length
  if (input.content?.text && input.content.text.length > 10000) {
    errors.push('Content text too long (max 10,000 characters)')
  }

  // Validate audience
  if (!input.audience || !Array.isArray(input.audience) || input.audience.length === 0) {
    errors.push('At least one audience member required')
  }

  // Validate timestamp
  if (input.published) {
    const now = Math.floor(Date.now() / 1000)
    if (input.published > now + 300) {  // 5 min tolerance
      errors.push('Published timestamp cannot be in the future')
    }
    if (input.published < now - 365 * 24 * 60 * 60) {  // 1 year ago
      errors.push('Published timestamp too far in the past')
    }
  }

  // Validate idTag format
  if (input.issuerTag && !validateIdTag(input.issuerTag)) {
    errors.push('Invalid issuer idTag format')
  }

  return {
    valid: errors.length === 0,
    errors
  }
}

function validateIdTag(idTag: string): boolean {
  // Format: local@domain
  const pattern = /^[a-z0-9._-]+@[a-z0-9.-]+\.[a-z]{2,}$/i
  return pattern.test(idTag)
}

Remember: Server-side validation is mandatory

// ⚠️ Client-side validation is for UX only
// Always validate on the server!
// The Cloudillo server performs validation on all inputs

Sanitize HTML Content

Prevent XSS attacks:

import DOMPurify from 'dompurify'

// ✅ Sanitize user-generated HTML
function sanitizeHtml(html: string): string {
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li', 'code', 'pre'],
    ALLOWED_ATTR: ['href', 'title'],
    ALLOW_DATA_ATTR: false
  })
}

// ✅ Render safely
function renderPost(post: Action) {
  const sanitized = sanitizeHtml(post.content.html || '')
  return <div dangerouslySetInnerHTML={{ __html: sanitized }} />
}

// ❌ NEVER render unsanitized user content
function renderPostUnsafe(post: Action) {
  return <div dangerouslySetInnerHTML={{ __html: post.content.html }} />
}

Escape text content:

function escapeHtml(text: string): string {
  const div = document.createElement('div')
  div.textContent = text
  return div.innerHTML
}

// Or use a library
import escape from 'lodash/escape'

// ✅ Safe text rendering
function renderText(text: string) {
  return <div>{text}</div>  // React escapes automatically
}

// ✅ Manual escaping if needed
const escaped = escape(userInput)

Prevent SQL/NoSQL Injection

Use parameterized queries (server-side):

// ✅ GOOD - Parameterized query (Cloudillo server does this)
let action = sqlx::query_as!(
    Action,
    "SELECT * FROM actions WHERE action_id = $1",
    action_id
)
.fetch_one(&pool)
.await?;

// ❌ NEVER - String concatenation
let query = format!("SELECT * FROM actions WHERE action_id = '{}'", action_id);

Client-side: Don’t construct queries

// ✅ GOOD - Use API endpoints
const action = await api.action.id(actionId).get()

// ❌ NEVER - Don't try to construct SQL from client
const query = `SELECT * FROM actions WHERE id = '${actionId}'`

Cross-Site Request Forgery (CSRF)

Token-Based CSRF Protection

Cloudillo uses JWT tokens for authentication, which provides CSRF protection when:

  1. Tokens are stored in Authorization header (not cookies)
  2. Requests are made from same origin
// ✅ SAFE - Token in Authorization header
fetch('/api/action', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(actionData)
})

// ⚠️ Additional protection for cookie-based auth
// Add CSRF token if using cookies
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content

fetch('/api/action', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': csrfToken,
    'Content-Type': 'application/json'
  },
  credentials: 'include',  // Include cookies
  body: JSON.stringify(actionData)
})

If using cookies (server sets these):

Set-Cookie: token=...; HttpOnly; Secure; SameSite=Strict
// Server-side cookie configuration
// SameSite=Strict: Cookie only sent for same-site requests
// SameSite=Lax: Cookie sent for top-level navigation
// SameSite=None: Cookie sent for all requests (requires Secure)

Content Security Policy (CSP)

Configure CSP Headers

Server-side CSP configuration:

<!-- In HTML meta tag -->
<meta http-equiv="Content-Security-Policy"
      content="default-src 'self';
               script-src 'self';
               style-src 'self' 'unsafe-inline';
               img-src 'self' data: https:;
               connect-src 'self' wss://your-server.com;">

Or via HTTP header (preferred):

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' wss://your-server.com;
  frame-ancestors 'none';

Report CSP Violations

Content-Security-Policy-Report-Only:
  default-src 'self';
  report-uri /csp-report
// Handle CSP reports (server-side)
app.post('/csp-report', (req, res) => {
  console.error('CSP Violation:', req.body)
  // Log to monitoring service
  res.status(204).end()
})

Rate Limiting and DoS Prevention

Client-Side Rate Limiting

class RateLimiter {
  private requests: number[] = []
  private limit: number
  private window: number  // milliseconds

  constructor(limit: number, window: number) {
    this.limit = limit
    this.window = window
  }

  canMakeRequest(): boolean {
    const now = Date.now()

    // Remove old requests outside window
    this.requests = this.requests.filter(time => now - time < this.window)

    return this.requests.length < this.limit
  }

  recordRequest() {
    this.requests.push(Date.now())
  }

  async throttle(): Promise<void> {
    if (!this.canMakeRequest()) {
      const oldestRequest = this.requests[0]
      const waitTime = this.window - (Date.now() - oldestRequest)

      if (waitTime > 0) {
        await new Promise(resolve => setTimeout(resolve, waitTime))
      }
    }

    this.recordRequest()
  }
}

// Usage
const limiter = new RateLimiter(100, 60000)  // 100 requests per minute

async function makeApiCall() {
  await limiter.throttle()
  return fetch('/api/action')
}

Handle Rate Limit Responses

async function fetchWithRateLimit(url: string, options?: RequestInit) {
  const response = await fetch(url, options)

  if (response.status === 429) {
    // Rate limited
    const retryAfter = response.headers.get('Retry-After')
    const waitSeconds = retryAfter ? parseInt(retryAfter) : 60

    console.warn(`Rate limited. Retry after ${waitSeconds} seconds`)

    await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000))

    // Retry request
    return fetchWithRateLimit(url, options)
  }

  return response
}

File Upload Security

Validate File Types

const ALLOWED_IMAGE_TYPES = [
  'image/jpeg',
  'image/png',
  'image/gif',
  'image/webp'
]

const ALLOWED_DOCUMENT_TYPES = [
  'application/pdf',
  'text/plain',
  'application/json'
]

function validateFileType(file: File, allowedTypes: string[]): boolean {
  if (!allowedTypes.includes(file.type)) {
    console.error(`Invalid file type: ${file.type}`)
    return false
  }

  // Check file extension as additional validation
  const ext = file.name.split('.').pop()?.toLowerCase()
  const validExtensions = {
    'image/jpeg': ['jpg', 'jpeg'],
    'image/png': ['png'],
    'image/gif': ['gif'],
    'image/webp': ['webp'],
    'application/pdf': ['pdf'],
    'text/plain': ['txt'],
    'application/json': ['json']
  }

  const allowedExts = validExtensions[file.type as keyof typeof validExtensions] || []
  if (!ext || !allowedExts.includes(ext)) {
    console.error(`Invalid file extension: ${ext}`)
    return false
  }

  return true
}

Validate File Size

const MAX_FILE_SIZE = 10 * 1024 * 1024  // 10MB

function validateFileSize(file: File): boolean {
  if (file.size > MAX_FILE_SIZE) {
    console.error(`File too large: ${file.size} bytes (max: ${MAX_FILE_SIZE})`)
    return false
  }

  return true
}

// Combined validation
function validateFile(file: File): ValidationResult {
  const errors: string[] = []

  if (!validateFileType(file, ALLOWED_IMAGE_TYPES)) {
    errors.push('Invalid file type. Please upload an image.')
  }

  if (!validateFileSize(file)) {
    errors.push(`File too large. Maximum size is ${MAX_FILE_SIZE / 1024 / 1024}MB.`)
  }

  return {
    valid: errors.length === 0,
    errors
  }
}

Scan Files for Malware

// Server-side malware scanning (example with ClamAV)
// This should be done on the server, not client
/*
async function scanFile(filePath: string): Promise<boolean> {
  const scanner = new ClamAV()
  const result = await scanner.scanFile(filePath)

  if (result.isInfected) {
    console.error('Malware detected:', result.viruses)
    await fs.unlink(filePath)  // Delete infected file
    return false
  }

  return true
}
*/

// Client can only do basic validation
// Real security must be server-side

Secure WebSocket Connections

Use WSS (WebSocket Secure)

// ✅ GOOD - Encrypted WebSocket
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
const ws = new WebSocket(`${protocol}//${location.host}/ws/rtdb/${fileId}?token=${token}`)

// ❌ NEVER in production - Unencrypted WebSocket
const ws = new WebSocket(`ws://example.com/ws/rtdb/${fileId}`)

Authenticate WebSocket Connections

class SecureWebSocket {
  private ws: WebSocket | null = null

  async connect(url: string) {
    // Get fresh token
    const token = await authManager.getValidToken()

    // Include token in connection
    this.ws = new WebSocket(`${url}?token=${token}`)

    this.ws.onopen = () => {
      console.log('WebSocket connected')
    }

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error)

      // Check if auth error (code 1008 = policy violation)
      // Refresh token and reconnect
    }

    this.ws.onmessage = (event) => {
      this.handleMessage(event.data)
    }
  }

  private handleMessage(data: string) {
    try {
      const message = JSON.parse(data)

      // Validate message structure
      if (!this.isValidMessage(message)) {
        console.error('Invalid message format')
        return
      }

      // Process message
      this.processMessage(message)
    } catch (err) {
      console.error('Failed to parse message:', err)
    }
  }

  private isValidMessage(message: any): boolean {
    return (
      typeof message === 'object' &&
      typeof message.type === 'string' &&
      message.data !== undefined
    )
  }
}

Validate WebSocket Messages

// Define allowed message types
const ALLOWED_MESSAGE_TYPES = ['update', 'delete', 'subscribe', 'unsubscribe']

function validateWebSocketMessage(message: any): boolean {
  // Check type
  if (!ALLOWED_MESSAGE_TYPES.includes(message.type)) {
    console.error('Invalid message type:', message.type)
    return false
  }

  // Validate based on type
  switch (message.type) {
    case 'update':
      return typeof message.path === 'string' && message.data !== undefined

    case 'delete':
      return typeof message.path === 'string'

    case 'subscribe':
    case 'unsubscribe':
      return typeof message.path === 'string'

    default:
      return false
  }
}

Secure Data Storage

Encrypt Sensitive Data

// Use Web Crypto API for encryption
async function encryptData(data: string, key: CryptoKey): Promise<string> {
  const encoder = new TextEncoder()
  const dataBuffer = encoder.encode(data)

  const iv = crypto.getRandomValues(new Uint8Array(12))

  const encrypted = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    key,
    dataBuffer
  )

  // Combine IV and encrypted data
  const combined = new Uint8Array(iv.length + encrypted.byteLength)
  combined.set(iv)
  combined.set(new Uint8Array(encrypted), iv.length)

  // Convert to base64
  return btoa(String.fromCharCode(...combined))
}

async function decryptData(encryptedData: string, key: CryptoKey): Promise<string> {
  // Convert from base64
  const combined = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0))

  // Extract IV and encrypted data
  const iv = combined.slice(0, 12)
  const data = combined.slice(12)

  const decrypted = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv },
    key,
    data
  )

  const decoder = new TextDecoder()
  return decoder.decode(decrypted)
}

// Generate encryption key
async function generateKey(): Promise<CryptoKey> {
  return await crypto.subtle.generateKey(
    { name: 'AES-GCM', length: 256 },
    true,  // extractable
    ['encrypt', 'decrypt']
  )
}

Clear Sensitive Data

class SecureDataHandler {
  private sensitiveData: string | null = null

  setSensitiveData(data: string) {
    this.sensitiveData = data
  }

  clearSensitiveData() {
    if (this.sensitiveData) {
      // Overwrite with random data before clearing
      const length = this.sensitiveData.length
      this.sensitiveData = Array(length)
        .fill(0)
        .map(() => String.fromCharCode(Math.random() * 256))
        .join('')

      // Clear
      this.sensitiveData = null
    }
  }

  // Clear on page unload
  cleanup() {
    window.addEventListener('beforeunload', () => {
      this.clearSensitiveData()
    })
  }
}

Security Headers

Set Appropriate Headers

Server should set these headers:

# Prevent MIME type sniffing
X-Content-Type-Options: nosniff

# Enable XSS protection
X-XSS-Protection: 1; mode=block

# Prevent clickjacking
X-Frame-Options: DENY

# Control referrer information
Referrer-Policy: strict-origin-when-cross-origin

# Enforce HTTPS
Strict-Transport-Security: max-age=31536000; includeSubDomains

# Permissions Policy
Permissions-Policy: geolocation=(), microphone=(), camera=()

Audit Logging

Log Security Events

class SecurityAuditor {
  async logAuthEvent(event: string, details: any) {
    await fetch('/api/audit/log', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        event,
        details,
        timestamp: Math.floor(Date.now() / 1000),
        userAgent: navigator.userAgent,
        ip: await this.getClientIP()
      })
    })
  }

  private async getClientIP(): Promise<string> {
    try {
      const response = await fetch('https://api.ipify.org?format=json')
      const { ip } = await response.json()
      return ip
    } catch {
      return 'unknown'
    }
  }
}

// Log security-relevant events
auditor.logAuthEvent('login_success', { email: user.email })
auditor.logAuthEvent('login_failure', { email: attemptedEmail })
auditor.logAuthEvent('token_refresh', { tokenId: oldTokenId })
auditor.logAuthEvent('permission_denied', { resource, action })

Dependency Security

Keep Dependencies Updated

# Check for vulnerabilities
npm audit

# Fix vulnerabilities automatically
npm audit fix

# Update dependencies
npm update

# Check for outdated packages
npm outdated

Use Dependency Scanning

# .github/workflows/security.yml
name: Security Scan

on: [push, pull_request]

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Run npm audit
        run: npm audit --audit-level=high

      - name: Run Snyk scan
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

Summary

Key security practices for Cloudillo applications:

  1. Authentication: Secure token storage, HTTPS only, proper validation
  2. Input Validation: Validate all user input, sanitize HTML, prevent injection
  3. CSRF Protection: Use Authorization header, SameSite cookies
  4. CSP: Configure Content Security Policy headers
  5. Rate Limiting: Implement client and server rate limiting
  6. File Upload: Validate type and size, scan for malware
  7. WebSocket: Use WSS, authenticate connections, validate messages
  8. Data Storage: Encrypt sensitive data, clear when done
  9. Security Headers: Set appropriate HTTP security headers
  10. Audit Logging: Log security events for monitoring
  11. Dependencies: Keep dependencies updated, scan for vulnerabilities

Security is a continuous process. Regularly review and update your security practices.