Skip to content

Documentation

Widget Security Implementation

Comprehensive security measures implemented across all SveltyCMS widgets to prevent XSS, injection attacks, and malicious content

4/10/2026
13 min read Edit on GitHub

This document details the comprehensive security measures implemented across all SveltyCMS widgets to prevent common vulnerabilities including XSS attacks, injection attacks, DoS attempts, and data leakage.

πŸ›‘οΈ Security Status Overview

Implementation Status: βœ… 12/12 Major Security Fixes Fully Implemented

All critical security vulnerabilities identified in the widget system have been addressed with proper validation, sanitization, and security controls. The latest addition adds binary MIME sniffing for media uploads.

Important

Centralized Dispatcher Gating: As of April 2026, all widget-specific API endpoints (e.g., /api/media, /api/collections/[name]/aggregations) are centrally gated by the Fail-Closed API Dispatcher. This ensures that even if an individual widget resolver lacks an internal check, the request is blocked unless the user possesses the required granular Permission ID (e.g., media:read, collections:read).


πŸ”’ Implemented Security Fixes

1. RemoteVideo Widget - SSRF Prevention βœ…

File: src/widgets/custom/remote-video/

Vulnerability: Server-Side Request Forgery (SSRF) allowing attackers to embed malicious URLs targeting internal services or metadata endpoints.

Implementation:

Client-Side Validation (input.svelte:58-65):

const ALLOWED_PLATFORMS = [
  /^https:\/\/(www\.)?youtube\.com\/watch\?v=[\w-]+$/,
  /^https:\/\/(www\.)?vimeo\.com\/\d+$/,
  /^https:\/\/(www\.)?twitch\.tv\/videos\/\d+$/,
  /^https:\/\/(www\.)?tiktok\.com\/@[\w.]+\/video\/\d+$/,
];

Server-Side Validation (index.ts:29-44):

const SAFE_VIDEO_URL_PATTERNS = [
  /^https:\/\/(www\.)?youtube\.com\/watch\?v=[\w-]+$/,
  /^https:\/\/(www\.)?vimeo\.com\/\d+$/,
  /^https:\/\/(www\.)?twitch\.tv\/videos\/\d+$/,
  /^https:\/\/(www\.)?tiktok\.com\/@[\w.]+\/video\/\d+$/,
];

// Blocks:
// - localhost URLs
// - Private IP addresses (10.x, 172.16-31.x, 192.168.x)
// - Cloud metadata endpoints (169.254.169.254)
// - Non-HTTPS protocols

Protection Against:

  • Internal service probing
  • Cloud metadata endpoint access (AWS/Azure/GCP)
  • Private network scanning
  • Protocol smuggling

2. Relation Widget - IDOR Protection βœ…

File: src/widgets/core/relation/index.ts

Vulnerability: Insecure Direct Object Reference (IDOR) allowing cross-tenant data access in multi-tenant deployments.

Implementation (Lines 77, 97):

// Accept tenantId parameter for tenant isolation
export async function aggregations(field: FieldInstance, tenantId?: string) {
  // ...

  // Apply tenant filtering in MongoDB aggregation
  pipeline.push({
    $match: {
      ...(tenantId && { tenantId }),
    },
  });
}

Protection Against:

  • Cross-tenant data leakage
  • Unauthorized relation lookups
  • Multi-tenant boundary violations

Implementation Details:

  • Tenant ID enforced at database query level
  • Applied to all relation aggregations
  • Prevents horizontal privilege escalation

3. Input Widget - ReDoS Protection βœ…

File: src/widgets/core/input/input.svelte

Vulnerability: Regular Expression Denial of Service (ReDoS) via extremely long input strings causing CPU exhaustion.

Implementation (Lines 60, 70):

const MAX_INPUT_LENGTH = 100000; // 100,000 characters

// Apply truncation before processing
if (value && value.length > MAX_INPUT_LENGTH) {
  value = value.substring(0, MAX_INPUT_LENGTH);
}

Protection Against:

  • CPU exhaustion from regex operations
  • Memory overflow from unbounded strings
  • Denial of Service attacks
  • Performance degradation

Rationale:

  • 100KB limit balances usability with security
  • Prevents pathological regex complexity
  • Maintains system responsiveness

4. SEO Widget - Meta Tag Injection Prevention βœ…

File: src/widgets/custom/seo/index.ts

Vulnerability: HTML injection in meta tags allowing malicious scripts in page metadata.

Implementation (Lines 34-52):

function escapeHtml(text: string): string {
  const map: { [key: string]: string } = {
    "&": "&",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#039;",
  };
  return text.replace(/[&<>"']/g, (m) => map[m]);
}

// Apply to all SEO fields
const schema = object({
  title: pipe(string(), transform(escapeHtml)),
  description: pipe(string(), transform(escapeHtml)),
  keywords: pipe(string(), transform(escapeHtml)),
  ogTitle: pipe(string(), transform(escapeHtml)),
  // ... all fields escaped
});

Protection Against:

  • Script injection in meta tags
  • XSS via Open Graph tags
  • SEO manipulation attacks
  • HTML entity exploitation

5. ColorPicker Widget - CSS Injection Prevention βœ…

File: src/widgets/custom/color-picker/display.svelte

Vulnerability: CSS injection allowing arbitrary CSS properties or expressions.

Implementation (Lines 26, 30):

function isValidHex(color: string): boolean {
  // Strict validation: only #RRGGBB format
  return /^#[0-9a-f]{6}$/i.test(color);
}

const safeColor = isValidHex(color) ? color : "#000000";

Protection Against:

  • CSS expression injection
  • Additional CSS properties (e.g., color: red; background: url(...))
  • Invalid color formats
  • CSS-based XSS attacks

Validation Rules:

  • Must start with #
  • Exactly 6 hexadecimal characters
  • Case-insensitive hex validation
  • Fallback to safe default (#000000)

6. Email Widget - Disposable Email Blocking βœ…

File: src/widgets/custom/email/index.ts

Vulnerability: Spam and abuse via temporary/disposable email addresses.

Implementation (Lines 26, 39, 50):

const DISPOSABLE_DOMAINS = [
  "tempmail.com",
  "guerrillamail.com",
  "10minutemail.com",
  "mailinator.com",
  "throwaway.email",
  "maildrop.cc",
  "temp-mail.org",
  "getnada.com",
];

const blockDisposableEmail = custom<string>((value) => {
  const domain = value.split("@")[1]?.toLowerCase();
  if (DISPOSABLE_DOMAINS.includes(domain)) {
    return { issue: { message: "Disposable email addresses are not allowed" } };
  }
  return { output: value };
});

// Applied in validation schema
const schema = pipe(string(), email(), blockDisposableEmail);

Protection Against:

  • Temporary email abuse
  • Spam account creation
  • Fake user registrations
  • Account abandonment

7. PhoneNumber Widget - E.164 International Validation βœ…

File: src/widgets/custom/phone-number/index.ts

Vulnerability: Malformed phone numbers bypassing validation, potential injection via special characters.

Implementation (Line 30):

// Improved E.164 international format validation
const phonePattern = /^\+[1-9]\d{1,3}[\d\s-]{4,14}$/;

// Validates:
// - Must start with + (international prefix)
// - Country code: 1-4 digits, cannot start with 0
// - Subscriber number: 4-14 characters (digits, spaces, hyphens)
// - Total length compliant with E.164 standard

Protection Against:

  • Invalid international formats
  • SQL injection via phone fields
  • Non-standard characters
  • Fake/test numbers

E.164 Compliance:

  • International prefix required (+)
  • Country code validation (1-999 range)
  • Length restrictions (max 15 digits total)
  • Allows formatting characters (space, hyphen)

8. Currency Widget - ISO 4217 Validation βœ…

File: src/widgets/custom/currency/index.ts

Vulnerability: Invalid currency codes causing financial calculation errors or injection.

Implementation (Lines 26, 102):

const VALID_CURRENCY_CODES = [
  "USD",
  "EUR",
  "GBP",
  "JPY",
  "CHF",
  "CAD",
  "AUD",
  "NZD",
  "CNY",
  "INR",
  "BRL",
  "RUB",
  "ZAR",
  "MXN",
  "SEK",
  "NOK",
  "DKK",
  "SGD",
  "HKD",
  "KRW",
  "TRY",
  "PLN",
  "THB",
  "IDR",
  "MYR",
  "PHP",
  "CZK",
  "HUF",
  "ILS",
  "CLP",
  "ARS",
  "COP",
];

// Validation schema with whitelist
const schema = object({
  code: pipe(
    string(),
    regex(/^[A-Z]{3}$/, "Must be 3 uppercase letters"),
    custom((value) => {
      if (!VALID_CURRENCY_CODES.includes(value)) {
        return { issue: { message: "Invalid currency code" } };
      }
      return { output: value };
    }),
  ),
});

Protection Against:

  • Invalid currency codes
  • Financial calculation errors
  • Arbitrary string injection
  • Database corruption

ISO 4217 Compliance:

  • Exactly 3 uppercase letters
  • Whitelisted valid codes only
  • Comprehensive major currency coverage

9. MegaMenu Widget - XSS Prevention βœ…

File: src/widgets/core/mega-menu/display.svelte

Vulnerability: Cross-Site Scripting (XSS) via malicious HTML in menu titles and descriptions.

Implementation (Lines 28, 40):

<script>
	import Sanitize from '@root/src/components/sanitize.svelte';
</script>

<!-- Menu title with strict sanitization -->
<Sanitize html={menuItem.title} profile="strict" />

Sanitization Profile (strict):

  • Removes all <script> tags
  • Blocks event handlers (onclick, onerror, etc.)
  • Strips dangerous tags (<iframe>, <object>, <embed>)
  • Allows only safe formatting tags (<b>, <i>, <em>, <strong>)

Protection Against:

  • Stored XSS attacks
  • DOM-based XSS
  • Script injection via menu content
  • Event handler exploitation

10. MediaUpload Widget - Comprehensive File Validation βœ…

File: src/widgets/core/media-upload/input.svelte

Vulnerability: Malicious file uploads bypassing validation (path traversal, oversized files, dangerous extensions).

Implementation (Lines 33-82, 178-243):

Security Constants:

const ALLOWED_MIME_TYPES = [
  // Images
  "image/jpeg",
  "image/jpg",
  "image/png",
  "image/gif",
  "image/webp",
  "image/svg+xml",
  "image/bmp",
  // Videos
  "video/mp4",
  "video/webm",
  "video/ogg",
  "video/quicktime",
  // Audio
  "audio/mpeg",
  "audio/mp3",
  "audio/wav",
  "audio/ogg",
  // Documents
  "application/pdf",
  "application/msword",
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  "application/vnd.ms-excel",
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
];

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

const VALID_EXTENSIONS = [
  ".jpg",
  ".jpeg",
  ".png",
  ".gif",
  ".webp",
  ".svg",
  ".bmp",
  ".mp4",
  ".webm",
  ".ogv",
  ".mov",
  ".mp3",
  ".wav",
  ".ogg",
  ".pdf",
  ".doc",
  ".docx",
  ".xls",
  ".xlsx",
];

Validation Checks (in order):

function addValidatedFiles(files: MediaFile[]) {
  const validFiles = files.filter((file) => {
    // 1. MIME Type Whitelist
    if (!ALLOWED_MIME_TYPES.includes(file.type)) {
      logger.warn(`[MediaUpload Security] File MIME type ${file.type} not in whitelist`);
      return false;
    }

    // 2. File Size Limit (DoS Prevention)
    if (file.size > MAX_FILE_SIZE) {
      logger.warn(`[MediaUpload Security] File size ${file.size} exceeds limit`);
      return false;
    }

    // 3. Extension Whitelist
    const fileExtension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0];
    if (!fileExtension || !VALID_EXTENSIONS.includes(fileExtension)) {
      logger.warn(`[MediaUpload Security] File extension ${fileExtension} not in whitelist`);
      return false;
    }

    // 4. Path Traversal Prevention
    if (file.name.includes("../") || file.name.includes("..\\")) {
      logger.warn(`[MediaUpload Security] Path traversal attempt detected`);
      return false;
    }

    // 5. Filename Sanitization
    const sanitizedName = file.name.replace(/[^a-zA-Z0-9._-]/g, "_");
    if (sanitizedName !== file.name) {
      logger.warn(`[MediaUpload Security] Filename sanitized`);
      file.name = sanitizedName;
    }

    // 6. Field-Level Allowed Types (Additional Restriction)
    const allowedTypes = field.allowedTypes as string[] | undefined;
    if (allowedTypes?.length > 0 && !allowedTypes.includes(file.type)) {
      logger.warn(`[MediaUpload Security] File type not in field allowedTypes`);
      return false;
    }

    return true;
  });
}

Protection Against:

  • Malicious Files: MIME type whitelist prevents executable uploads
  • DoS Attacks: 10MB size limit prevents resource exhaustion
  • Extension Spoofing: Double-checks extension matches MIME type
  • Path Traversal: Blocks ../ directory escape attempts
  • Command Injection: Filename sanitization removes dangerous characters
  • Per-Field Restrictions: Honors collection-specific allowed types

Security Features:

  • Layered Validation: 6 independent security checks
  • Development Logging: All rejections logged with [MediaUpload Security] prefix
  • Safe Defaults: Falls back to empty array on validation failure
  • Audit Trail: All security events tracked in development mode

11. Media Asset Publish-State Gating βœ…

Files:

  • src/utils/media/media-service.server.ts β€” getPublishedReferences() / isReferencedByPublishedContent()
  • src/services/sdk/namespaces/media-namespace.ts β€” SDK layer exposure
  • src/routes/api/[...path]/handlers/media.ts β€” Handler-level gate checks
  • src/routes/(app)/mediagallery/ β€” UI-layer disabled states (grid, table, virtual grid)

Vulnerability: Media assets referenced by published content entries could be deleted or edited, causing broken references on published pages.

Implementation (4-Layer Defense-in-Depth):

// Layer 1 β€” Service: Queries collection_* tables for entries with status="publish"
// that reference the given mediaId (by ID, path, URL, or embedded object)
const refs = await mediaService.getPublishedReferences(mediaId, tenantId);
// Returns: [{ collectionId, collectionName, entryId, entryName, fieldName }]

// Layer 2 β€” Handler: 409 Conflict before any mutation
await checkMediaNotReferencedByPublishedContent(cms, mediaId, tenantId);
// Throws AppError with detailed references summary

// Layer 3 β€” UI: Disabled edit/delete buttons + tooltip + lock badge
// See media-grid.svelte: disabled={isPublishedReferenced}

// Layer 4 β€” Server Load: Batched pre-check for all visible media items
const results = await Promise.allSettled(
  processedMedia.map((item) =>
    mediaService.isReferencedByPublishedContent(item._id, tenantId)
  )
);

Protection Against:

  • Broken references on published pages after media deletion
  • Orphaned media mutations affecting live content
  • UI confusion β€” clear β€œReferenced by published content” feedback

Gated Mutation Handlers: | Handler | Operation | Status | |---|---|---| | handleDeleteRoutes | Direct asset deletion | 409 Conflict | | handleMediaPostDelete | POST-based deletion | 409 Conflict | | handleMediaManipulate | Image manipulation | 409 Conflict | | handleMediaVersionCreate | New version creation | 409 Conflict | | handleMediaVersionUpload | Version file upload | 409 Conflict | | handleMediaVersionRestore | Version restoration | 409 Conflict |

UI Indicators:

  • Edit button: disabled + tooltip β€œReferenced by published content”
  • Delete button: disabled + tooltip β€œReferenced by published content”
  • Card badge: Lock icon + β€œPublished” label at top-left

12. Media Upload - Binary MIME Sniffing βœ…

Files: src/utils/media/media-service.server.ts, src/utils/media/slim-sniffer.server.ts

Vulnerability: MIME type spoofing β€” attackers could upload a file with a benign MIME type (e.g., image/png) while the file content is actually executable or malicious (e.g., PHP shell).

Implementation:

  • Small files (<5MB): Uses arrayBuffer() β†’ Buffer.from() β†’ sniffMimeType(buffer.subarray(0, 2048)) to verify binary signatures before accepting
  • Large files (β‰₯5MB): Reads first 2048 bytes via file.slice(0, 2048).arrayBuffer(), runs sniffMimeType(), and rejects on major MIME category mismatch (e.g., client sends image/png but binary signature indicates application/pdf)
  • Uses the lightweight slim-sniffer.server.ts (~2KB) which detects 10+ formats via magic bytes (JPEG, PNG, GIF, WebP, SVG, MP4, WebM, PDF, DOCX)

Protection Against:

  • MIME type spoofing attacks
  • Uploading executables disguised as media files
  • Content-type confusion attacks
  • Zero-byte and truncated file uploads

Implementation Details:

  • Binary signatures verified server-side before any processing
  • Major category mismatches (image vs video vs document) trigger rejection
  • SVGs additionally sanitized via dedicated sanitizeSvg() function

πŸ” Security Testing

All security fixes have been validated through:

  1. Code Review: Each implementation reviewed against OWASP guidelines
  2. Security Comments: All fixes include [Widget Security] comments
  3. Logging: Development mode logging for security events
  4. Pattern Consistency: Similar vulnerabilities use consistent patterns

Test Coverage

# Security test suite
bun test tests/bun/widgets/widget-security.test.ts

# Individual widget security tests
bun test tests/bun/widgets/custom-widgets.test.ts

Test Categories:

  • βœ… SSRF prevention (RemoteVideo)
  • βœ… IDOR prevention (Relation)
  • βœ… ReDoS prevention (Input)
  • βœ… Injection prevention (SEO, ColorPicker)
  • βœ… XSS prevention (MegaMenu, RichText)
  • βœ… File upload validation (MediaUpload)
  • βœ… SVG sanitization (MediaUpload server-side)
  • βœ… Format validation (Email, PhoneNumber, Currency)

πŸ“‹ Security Checklist

Use this checklist when creating new widgets:

Input Validation

  • MIME type whitelist (file widgets)
  • File size limits (upload widgets)
  • Extension validation (file widgets)
  • Path traversal prevention
  • Input length limits (text widgets)
  • Format validation (email, phone, URL)
  • Regex pattern validation
  • Whitelist-based validation

Output Encoding

  • HTML entity escaping
  • CSS value sanitization
  • URL encoding
  • JavaScript string escaping
  • SQL parameterization

XSS Prevention

  • Use Sanitize component for HTML
  • Escape user-generated content
  • Content Security Policy compliance
  • Avoid {@html} without sanitization
  • Validate URLs before rendering

SSRF Prevention

  • URL protocol whitelist (https only)
  • Domain whitelist
  • Block private IPs
  • Block localhost
  • Block cloud metadata endpoints

IDOR Prevention

  • Tenant ID filtering
  • User ID validation
  • Permission checks
  • Relation validation

DoS Prevention

  • Input length limits
  • File size limits
  • Rate limiting considerations
  • Regex complexity limits
  • Resource consumption limits

Media Integrity

  • Published-reference gate for asset mutations
  • Check all collection entries with status=β€œpublish” before delete/edit
  • UI disabled state with contextual tooltip
  • Batched pre-check on server load

πŸ› οΈ Security Utilities

Sanitize Component

File: src/components/sanitize.svelte

<Sanitize html={userContent} profile="strict" />

Profiles:

  • strict - Only safe formatting tags (recommended for user content)
  • basic - Allows common HTML tags
  • custom - Define your own allowed tags

HTML Escaping

function escapeHtml(text: string): string {
  const map = {
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#039;",
  };
  return text.replace(/[&<>"']/g, (m) => map[m]);
}

URL Validation

function isValidUrl(url: string, allowedProtocols = ["https"]): boolean {
  try {
    const parsed = new URL(url);
    return allowedProtocols.includes(parsed.protocol.replace(":", ""));
  } catch {
    return false;
  }
}

πŸ“š References

Security Standards

Widget Documentation

Related Security Docs


βœ… Compliance

All implemented security measures comply with:

  • GDPR: Secure data handling and validation
  • SOC 2: Security controls and audit logging
  • OWASP: Top 10 vulnerability prevention
  • PCI DSS: (If handling payments) Secure data validation

securitywidgetsxssvalidationsanitization
Was this page helpful?