Skip to content

Security

When to Use

Always sanitize AI output on server. Never expose API keys client-side. Validate file uploads server-side with magic bytes. Implement rate limiting.

XSS Prevention

The Problem:

AI responses can contain malicious HTML/JavaScript. DeepChat renders html responses directly in the DOM.

Backend Sanitization (Required):

// Drupal example
use Drupal\Component\Utility\Xss;

$allowedTags = [
  'a', 'b', 'blockquote', 'br', 'code', 'del', 'em',
  'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img',
  'li', 'ol', 'p', 'pre', 'strong', 'ul',
];

$sanitizedResponse = Xss::filter($aiResponse, $allowedTags);

Next.js/Node.js:

import sanitizeHtml from 'sanitize-html';

const allowedTags = [
  'a', 'b', 'blockquote', 'br', 'code', 'del', 'em',
  'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img',
  'li', 'ol', 'p', 'pre', 'strong', 'ul',
];

export async function POST(request: NextRequest) {
  const aiResponse = await callAI();

  const sanitized = sanitizeHtml(aiResponse, {
    allowedTags,
    allowedAttributes: {
      'a': ['href', 'target'],
      'img': ['src', 'alt'],
    },
  });

  return NextResponse.json({ html: sanitized });
}

Content Security Policy:

// middleware.ts
export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  response.headers.set(
    'Content-Security-Policy',
    [
      "default-src 'self'",
      "script-src 'self' 'unsafe-inline'", // DeepChat needs inline
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
      "connect-src 'self' https://api.openai.com",
    ].join('; ')
  );

  return response;
}

API Key Management

Never Expose Keys:

// ❌ WRONG - API key exposed
<DeepChat
  directConnection={{
    openai: {
      key: 'sk-...',
    },
  }}
/>

// ✅ CORRECT - Key on server
// app/api/chat/route.ts
const response = await fetch('https://api.openai.com/v1/chat/completions', {
  headers: {
    'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
  },
});

Environment Variables:

# .env.local (NEVER commit)
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
DRUPAL_BASE_URL=https://drupal.example.com

Runtime Validation:

// lib/config.ts
const config = {
  openaiApiKey: process.env.OPENAI_API_KEY,
  drupalBaseUrl: process.env.DRUPAL_BASE_URL,
};

// Validate on startup
if (!config.openaiApiKey) {
  throw new Error('OPENAI_API_KEY is required');
}

export default config;

File Upload Security

Validate File Type:

export async function POST(request: NextRequest) {
  const formData = await request.formData();
  const files = formData.getAll('files') as File[];

  for (const file of files) {
    // Check MIME type
    if (!file.type.startsWith('image/')) {
      return NextResponse.json(
        { error: 'Only images allowed' },
        { status: 400 }
      );
    }

    // Check size (5MB limit)
    if (file.size > 5 * 1024 * 1024) {
      return NextResponse.json(
        { error: 'File too large' },
        { status: 400 }
      );
    }

    // Check magic bytes (real validation)
    const buffer = Buffer.from(await file.arrayBuffer());
    const isPNG = buffer[0] === 0x89 && buffer[1] === 0x50;
    const isJPG = buffer[0] === 0xFF && buffer[1] === 0xD8;

    if (!isPNG && !isJPG) {
      return NextResponse.json(
        { error: 'Invalid file format' },
        { status: 400 }
      );
    }
  }

  // Process files...
}

Scan for Malware:

import { scan } from 'clamav.js';

const result = await scan(buffer);
if (result.isInfected) {
  return NextResponse.json(
    { error: 'File contains malware' },
    { status: 400 }
  );
}

Rate Limiting

API Route Protection:

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '1 m'), // 10 requests per minute
});

export async function POST(request: NextRequest) {
  const ip = request.ip ?? '127.0.0.1';
  const { success } = await ratelimit.limit(ip);

  if (!success) {
    return NextResponse.json(
      { error: 'Rate limit exceeded' },
      { status: 429 }
    );
  }

  // Process request...
}

User-Based Limits:

const session = await auth();
const userId = session?.user?.id ?? 'anonymous';

const { success, remaining } = await ratelimit.limit(userId);

if (!success) {
  return NextResponse.json(
    { error: `Rate limit exceeded. ${remaining} requests remaining.` },
    { status: 429 }
  );
}

Common Mistakes

  • Wrong: Trusting AI output is safe → Right: AI can output malicious HTML. Always sanitize
  • Wrong: Sanitizing on client side → Right: Client can bypass. Sanitize on server
  • Wrong: Using dangerouslySetInnerHTMLRight: XSS risk. Let DeepChat handle rendering
  • Wrong: Not validating file uploads → Right: Users can upload executables. Check magic bytes
  • Wrong: Storing API keys in frontend → Right: Exposed in bundle. Always use server routes
  • Wrong: No rate limiting → Right: API abuse and cost explosion. Implement limits
  • Wrong: Weak CSP → Right: XSS still possible. Use strict CSP

See Also

  • Best Practices
  • Anti-Patterns
  • Reference: https://web.dev/articles/strict-csp
  • Reference: https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html
  • Reference: https://owasp.org/www-community/attacks/xss/