Skip to content

Remote Resource Security

When to Use

You're loading icons from CDNs or external sources and need to understand SSRF, mixed content, and availability risks.

Decision

Risk Mitigation Applies to extractor
SSRF (Server-Side Request Forgery) Whitelist domains, validate URLs Path, SVG Sprite
Mixed content (HTTP/HTTPS) Enforce HTTPS, use SRI Path, SVG Sprite
Availability (CDN down) Fallback to local, monitoring Path, SVG Sprite
Content tampering Subresource Integrity (SRI) SVG Sprite via library
Privacy (tracking pixels) Proxy through your server Path

Pattern

Secure remote icon configuration:

# ⚠️ Remote sprites require validation
cdn_sprites:
  extractor: svg_sprite
  config:
    sources:
      - https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/bootstrap-icons.svg
  library: "my_theme/bootstrap_icons"  # Include SRI in library

# ❌ Never allow user-controlled URLs
# Bad: sources: ["{{ user_input_url }}"]

Library with Subresource Integrity:

# my_theme.libraries.yml
bootstrap_icons:
  remote: https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3
  css:
    theme:
      https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css:
        type: external
        attributes:
          integrity: sha384-[hash]
          crossorigin: anonymous

Validate remote URLs in custom extractor:

<?php
namespace Drupal\my_module\Plugin\IconExtractor;

use Drupal\Core\Theme\Icon\IconExtractorBase;

class SecureRemoteExtractor extends IconExtractorBase {

  private const ALLOWED_DOMAINS = [
    'cdn.jsdelivr.net',
    'cdnjs.cloudflare.com',
  ];

  protected function validateUrl(string $url): bool {
    $parsed = parse_url($url);

    // Require HTTPS
    if ($parsed['scheme'] !== 'https') {
      return FALSE;
    }

    // Whitelist domains
    if (!in_array($parsed['host'], self::ALLOWED_DOMAINS, TRUE)) {
      return FALSE;
    }

    return TRUE;
  }

  public function discoverIcons(): array {
    $sources = $this->configuration['sources'] ?? [];

    foreach ($sources as $source) {
      if (!$this->validateUrl($source)) {
        \Drupal::logger('my_module')->error(
          'Blocked icon source from non-whitelisted domain: @url',
          ['@url' => $source]
        );
        continue;
      }

      // Process validated source
    }
  }
}

Fallback to local icons:

icons_with_fallback:
  extractor: svg_sprite
  config:
    sources:
      - https://cdn.example.com/icons.svg  # Try CDN first
      - sprites/icons-local.svg  # Fallback to local

Monitor external dependencies:

// In hook_cron or custom service
function my_module_check_icon_availability() {
  $urls = [
    'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/bootstrap-icons.svg',
  ];

  foreach ($urls as $url) {
    $response = \Drupal::httpClient()->head($url);

    if ($response->getStatusCode() !== 200) {
      \Drupal::logger('my_module')->warning(
        'Icon CDN unavailable: @url',
        ['@url' => $url]
      );
    }
  }
}

Reference: OWASP SSRF Prevention Cheat Sheet

Common Mistakes

  • Wrong: No domain whitelist → Right: Allows SSRF to internal services (localhost, 169.254.169.254)
  • Wrong: HTTP instead of HTTPS → Right: Mixed content warnings, MITM attacks
  • Wrong: No SRI for external resources → Right: CDN compromise injects malicious code
  • Wrong: Single point of failure → Right: No fallback when CDN is down
  • Wrong: Not monitoring CDN availability → Right: Icons break silently when CDN changes/removes files

See Also