IT Security 1 month ago

Contao CMS Feed Reader SSRF Vulnerability – Detection & Mitigation Guide

A critical SSRF flaw in Contao CMS's Feed Reader module lets authenticated users request internal services, exposing cloud metadata and internal networks. This guide details detection methods and step‑by‑step mitigation. A critical SSRF flaw in Contao CMS's Feed Reader module lets authenticated users request internal services, exposing cloud metadata and internal networks. This guide details detection methods and…

IT Security

Executive Summary

A critical Server-Side Request Forgery (SSRF) vulnerability exists in the Contao CMS Feed Reader module, affecting all versions that lack proper request validation. Authenticated backend users with module-edit permissions can configure arbitrary RSS feed URLs to access internal network services, cloud metadata endpoints, and sensitive infrastructure. This advisory provides IT security teams with actionable detection and remediation procedures to eliminate this risk.

Key Risk Factors:

  • Exploitation requires only backend authentication (lower barrier than unauthenticated attacks)
  • Access to cloud metadata services can expose IAM credentials
  • Internal network enumeration enables lateral movement
  • No network-level protections can prevent this attack

Vulnerability Overview

What is the Contao CMS SSRF Vulnerability

The Contao CMS Feed Reader module processes user-supplied RSS feed URLs without validating whether those URLs target internal or external resources. The vulnerable getResponse() function in FeedReaderController.php passes feed URLs directly to an HTTP client that lacks SSRF protections, allowing backend users to craft requests to:

  • Private IP address ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
  • Loopback addresses (127.0.0.1, ::1)
  • Cloud metadata endpoints (169.254.169.254)
  • Internal APIs and databases
  • Administrative interfaces

This vulnerability is classified as CWE-918: Server-Side Request Forgery (SSRF) and represents a critical control bypass.

Affected Versions and Components

Affected Product: Contao CMS (all versions)

Vulnerable Component: Feed Reader module (FeedReaderController.php)

Exploitation Requirements:

  • Valid backend user account
  • Module-edit permissions assigned to user role
  • Access to Feed Reader module configuration interface

Note: Versions without explicit SSRF protections in the HTTP client implementation are vulnerable. Check your FeedReaderController.php for the presence of NoPrivateNetworkHttpClient wrapper.

Severity and Risk Assessment

Factor Assessment
Attack Complexity Low (requires only backend access)
Privileges Required Medium (authenticated backend user)
User Interaction None
Scope Changed (can access out-of-scope resources)
Confidentiality Impact High (internal data exposure)
Integrity Impact Medium (potential for internal system manipulation)
Availability Impact Medium (DoS against internal services)
Overall Severity CRITICAL

Technical Details

Root Cause: FeedReaderController.php Flaw

The vulnerability originates from insufficient input validation in the Feed Reader module’s HTTP request handling. The vulnerable code pattern resembles:

// VULNERABLE CODE EXAMPLE
public function getResponse($feedUrl)
{
    // No URL validation performed
    // No SSRF protection implemented
    $response = $this->httpClient->request('GET', $feedUrl);
    return $response;
}

The HTTP client used by default does not implement protections against private network access, allowing requests to reach any network-accessible resource from the server’s perspective.

How the Vulnerability Works

Attack Flow:

  1. Attacker Authentication: Threat actor logs into Contao backend with valid credentials
  2. Module Access: Navigates to Feed Reader module configuration
  3. Malicious URL Injection: Enters crafted URL targeting internal resource:
  • http://192.168.1.50:5432/ (internal database)
  • http://169.254.169.254/latest/meta-data/iam/security-credentials/ (AWS metadata)
  • http://localhost:8080/admin/ (internal admin panel)
  1. Request Execution: Feed Reader module processes the URL without validation
  2. Response Retrieval: Server makes HTTP request from its network context
  3. Data Exfiltration: Response data returned to attacker or logged in accessible locations

Why This Matters:

The Contao server occupies a trusted position within your network infrastructure. Requests originating from it bypass firewall rules, network segmentation, and access controls designed for external threats. An attacker leverages this trust to access resources they couldn’t reach directly.

Why Standard HTTP Clients Are Insufficient

Generic HTTP client libraries (cURL, Guzzle, Symfony HttpClient) are designed for flexibility, not security. They will happily make requests to:

  • Private IP ranges
  • Loopback addresses
  • Reserved IP blocks
  • Cloud metadata services

Solution: Wrap the HTTP client with Symfony’s NoPrivateNetworkHttpClient, which explicitly blocks requests to non-routable addresses before they’re sent.


Attack Scenarios and Business Impact

Internal Network Enumeration

Scenario: A disgruntled employee with backend access systematically probes internal infrastructure.

Attack Steps:

  1. Configure Feed Reader with URLs targeting common internal IP ranges
  2. Observe response times and error messages
  3. Map active services: http://192.168.1.1/, http://192.168.1.50:5432/, etc.
  4. Identify service types based on response headers and content

Business Impact:

  • Complete internal network topology exposed
  • Enables targeted attacks against identified services
  • Facilitates lateral movement planning

Unauthorized Access to Internal APIs and Databases

Scenario: Attacker accesses internal PostgreSQL database through Feed Reader.

Attack Steps:

  1. Configure Feed Reader URL: http://internal-db.local:5432/
  2. Observe database connection responses
  3. Attempt to extract data through error-based techniques
  4. Access internal APIs: http://internal-api.local:8080/users/list

Business Impact:

  • Direct database compromise without authentication
  • Extraction of customer data, financial records, credentials
  • Potential regulatory violations (GDPR, HIPAA, PCI-DSS)
  • Reputational damage from data breach

Cloud Metadata Credential Theft

Scenario: Contao instance running on AWS EC2 instance; attacker retrieves IAM credentials.

Attack Steps:

  1. Configure Feed Reader URL: http://169.254.169.254/latest/meta-data/iam/security-credentials/
  2. Retrieve IAM role name from metadata service
  3. Access credentials endpoint: http://169.254.169.254/latest/meta-data/iam/security-credentials/[ROLE-NAME]
  4. Obtain temporary AWS credentials with associated permissions

Business Impact:

  • Complete AWS account compromise
  • Ability to launch instances, access S3 buckets, modify databases
  • Potential for cryptomining, data destruction, or ransomware deployment
  • Estimated cost of incident: $100,000+ in unauthorized resource usage

Lateral Movement and Infrastructure Pivoting

Scenario: Attacker uses compromised Contao server as pivot point for internal network exploitation.

Attack Steps:

  1. Establish initial access through SSRF vulnerability
  2. Enumerate internal services and identify high-value targets
  3. Use Contao server to probe security controls and firewall rules
  4. Pivot to domain controller, backup systems, or payment processing infrastructure
  5. Establish persistent backdoors on critical systems

Business Impact:

  • Enterprise-wide compromise
  • Extended dwell time for attackers (weeks/months undetected)
  • Compromise of backup systems preventing recovery
  • Potential for supply chain attacks if Contao hosts customer-facing content

Detection and Monitoring

Log Analysis Techniques

1. Web Server Access Logs

Search for Feed Reader module requests with suspicious URL parameters:

# Apache/Nginx access logs
grep "module=feed" access.log | grep -E "(192\.168|10\.|172\.1[6-9]\.|127\.0\.0\.1|169\.254)"
# Look for encoded URLs
grep "module=feed" access.log | grep -E "(%2F|%3A|%2E)" | head -20

2. Application Logs

Monitor Contao system logs for Feed Reader errors:

# Check var/logs/prod.log for HTTP client errors
tail -f var/logs/prod.log | grep -i "feed\|http\|connection"
# Look for connection timeouts (indicator of internal service probing)
grep "timeout\|refused\|unreachable" var/logs/prod.log

3. PHP Error Logs

# Monitor for network-related errors
grep -E "fsockopen|stream_socket|curl.*connect" /var/log/php-errors.log

Network Traffic Indicators

Outbound Connection Patterns to Monitor:

# Private IP ranges
Destination: 10.0.0.0/8
Destination: 172.16.0.0/12
Destination: 192.168.0.0/16
# Loopback addresses
Destination: 127.0.0.1/8
Destination: ::1/128
# Cloud metadata endpoints
Destination: 169.254.169.254
Destination: 169.254.169.253 (Azure)
Destination: 100.100.100.200 (Alibaba)
# Reserved ranges
Destination: 0.0.0.0/8
Destination: 224.0.0.0/4 (Multicast)

Detection Rules (Suricata/Snort):

alert http $HOME_NET any -> $HOME_NET any (
  msg:"Possible SSRF via Contao Feed Reader";
  content:"GET"; http_method;
  content:"module=feed"; http_uri;
  content:"rss_feed="; http_uri;
  pcre:"/rss_feed=.*?(192\.168|10\.|172\.1[6-9]\.|127\.0\.0\.1|169\.254)/";
  sid:1000001; rev:1;
)

Vulnerability Scanner Configuration

Nessus Plugin Configuration:

# Enable SSRF detection plugins
Plugin ID 159037: HTTP Server SSRF Vulnerability
Plugin ID 159038: SSRF via URL Parameter
# Configure for Contao-specific detection
Custom plugin: Check for NoPrivateNetworkHttpClient in FeedReaderController.php

Manual Verification Script:

#!/bin/bash
# Check if Contao installation is vulnerable
CONTAO_PATH="/var/www/contao"
CONTROLLER_FILE="$CONTAO_PATH/vendor/contao/core-bundle/src/Controller/FeedReaderController.php"
if grep -q "NoPrivateNetworkHttpClient" "$CONTROLLER_FILE"; then
    echo "[PASS] SSRF protection implemented"
else
    echo "[FAIL] SSRF protection NOT found - system is vulnerable"
fi

Identifying Suspicious Feed URL Patterns

Database Query to Find Suspicious Configurations:

-- MySQL/MariaDB
SELECT id, name, rss_feed FROM tl_module 
WHERE type='rss_reader' 
AND rss_feed REGEXP '(192\.168|10\.|172\.1[6-9]\.|127\.0\.0\.1|169\.254|localhost|internal)'
ORDER BY tstamp DESC;
-- PostgreSQL
SELECT id, name, rss_feed FROM tl_module 
WHERE type='rss_reader' 
AND rss_feed ~ '(192\.168|10\.|172\.1[6-9]\.|127\.0\.0\.1|169\.254|localhost|internal)'
ORDER BY tstamp DESC;

Indicators of Exploitation:

  • Feed URLs pointing to private IP ranges
  • Feed URLs using non-standard ports (5432, 3306, 8080, 9200)
  • Feed URLs containing encoded characters or special syntax
  • Multiple failed Feed Reader requests followed by successful internal connections
  • Feed Reader module accessed outside normal business hours
  • Rapid sequential requests to different internal IP addresses

Immediate Mitigation Steps

Implement Symfony NoPrivateNetworkHttpClient

Step 1: Update FeedReaderController.php

<?php
// File: vendor/contao/core-bundle/src/Controller/FeedReaderController.php
namespace Contao\CoreBundle\Controller;
use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class FeedReaderController
{
    private HttpClientInterface $httpClient;
    public function __construct(HttpClientInterface $httpClient)
    {
        // WRAP the HTTP client with SSRF protection
        $this->httpClient = new NoPrivateNetworkHttpClient($httpClient);
    }
    public function getResponse(string $feedUrl): ResponseInterface
    {
        // This will now throw an exception for private network requests
        try {
            $response = $this->httpClient->request('GET', $feedUrl, [
                'timeout' => 10,
                'headers' => [
                    'User-Agent' => 'Contao Feed Reader/1.0',
                ]
            ]);
            return $response;
        } catch (TransportExceptionInterface $e) {
            // Log and handle SSRF attempt
            throw new \RuntimeException('Invalid feed URL: ' . $e->getMessage());
        }
    }
}

Step 2: Verify Implementation

# Test that private network requests are blocked
php -r "
require 'vendor/autoload.php';
use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient;
use Symfony\Component\HttpClient\HttpClient;
\$client = new NoPrivateNetworkHttpClient(HttpClient::create());
try {
    \$client->request('GET', 'http://192.168.1.1/');
} catch (\Exception \$e) {
    echo 'SSRF Protection Active: ' . \$e->getMessage();
}
"

Enforce URL Scheme Validation

Add URL Validation Function:

<?php
// File: src/Security/FeedUrlValidator.php
namespace App\Security;
class FeedUrlValidator
{
    /**
     * Validate feed URL before processing
     * 
     * @param string $url
     * @return bool
     * @throws \InvalidArgumentException
     */
    public static function validate(string $url): bool
    {
        // Parse URL
        $parsed = parse_url($url);
        if (!$parsed) {
            throw new \InvalidArgumentException('Invalid URL format');
        }
        // 1. Enforce HTTP/HTTPS only
        if (!in_array($parsed['scheme'] ?? '', ['http', 'https'])) {
            throw new \InvalidArgumentException('Only HTTP/HTTPS schemes allowed');
        }
        // 2. Extract hostname
        $host = $parsed['host'] ?? '';
        // 3. Reject private IP ranges
        $privateRanges = [
            '10.0.0.0/8',
            '172.16.0.0/12',
            '192.168.0.0/16',
            '127.0.0.0/8',
            '169.254.0.0/16',
            '224.0.0.0/4',
            '240.0.0.0/4',
        ];
        $ip = gethostbyname($host);
        foreach ($privateRanges as $range) {
            if (self::ipInRange($ip, $range)) {
                throw new \InvalidArgumentException(
                    "URL targets private network: $ip"
                );
            }
        }
        // 4. Reject localhost variations
        if (in_array($host, ['localhost', 'localhost.localdomain', '::1'])) {
            throw new \InvalidArgumentException('Localhost URLs not permitted');
        }
        return true;
    }
    /**
     * Check if IP is within CIDR range
     */
    private static function ipInRange(string $ip, string $range): bool
    {
        [$subnet, $bits] = explode('/', $range);
        $ip = ip2long($ip);
        $subnet = ip2long($subnet);
        $mask = -1 << (32 - $bits);
        $subnet &= $mask;
        return ($ip & $mask) === $subnet;
    }
}

Integrate Validation into Feed Reader:

<?php
// In FeedReaderController.php
public function getResponse(string $feedUrl): ResponseInterface
{
    // Validate URL before processing
    try {
        FeedUrlValidator::validate($feedUrl);
    } catch (\InvalidArgumentException $e) {
        \System::log(
            'SSRF attempt blocked: ' . $e->getMessage(),
            __METHOD__,
            TL_ERROR
        );
        throw new \RuntimeException('Invalid feed URL');
    }
    // Proceed with safe HTTP request
    return $this->httpClient->request('GET', $feedUrl);
}

Configure DCA Field Restrictions

Update Data Container Array (DCA) Configuration:

<?php
// File: contao/dca/tl_module.php
$GLOBALS['TL_DCA']['tl_module']['fields']['rss_feed'] = [
    'label'                   => &$GLOBALS['TL_LANG']['tl_module']['rss_feed'],
    'exclude'                 => true,
    'inputType'               => 'text',
    'eval'                    => [
        'mandatory'           => true,
        'rgxp'                => 'url',
        'decodeEntities'      => true,
        'maxlength'           => 255,
        'tl_class'            => 'w50',
        'custom'              => 'validateFeedUrl', // Custom validator
    ],
    'save_callback'           => [
        ['App\Callbacks\FeedUrlCallback', 'validateAndSanitize']
    ],
];

Create Callback Class:

<?php
// File: src/Callbacks/FeedUrlCallback.php
namespace App\Callbacks;
use App\Security\FeedUrlValidator;
class FeedUrlCallback
{
    public function validateAndSanitize($value, $dc)
    {
        try {
            FeedUrlValidator::validate($value);
        } catch (\InvalidArgumentException $e) {
            throw new \Exception('Invalid feed URL: ' . $e->getMessage());
        }
        return $value;
    }
}

Restrict Backend Module Permissions

Implement Role-Based Access Control:

<?php
// File: contao/dca/tl_user.php
// Add Feed Reader module to permission checks
$GLOBALS['TL_DCA']['tl_user']['fields']['modules']['options_callback'] = 
    ['App\Callbacks\UserPermissionCallback', 'getModules'];
// Restrict who can edit Feed Reader module
$GLOBALS['TL_DCA']['tl_user']['fields']['modules']['eval']['mandatory'] = true;

Create Permission Callback:

<?php
// File: src/Callbacks/UserPermissionCallback.php
namespace App\Callbacks;
class UserPermissionCallback
{
    public function getModules()
    {
        $modules = [];
        // Get all available modules
        foreach ($GLOBALS['FE_MOD'] as $group => $mods) {
            foreach ($mods as $key => $mod) {
                // Restrict Feed Reader to admin users only
                if ($key === 'rss_reader') {
                    // Only show for users with admin role
                    if ($GLOBALS['TL_USER']->isAdmin) {
                        $modules[$group][$key] = $mod;
                    }
                } else {
                    $modules[$group][$key] = $mod;
                }
            }
        }
        return $modules;
    }
}

Database-Level Permission Enforcement:

-- Restrict Feed Reader module access to admin role only
UPDATE tl_user_group 
SET modules = REPLACE(modules, 's:10:"rss_reader"', '')
WHERE id NOT IN (SELECT id FROM tl_user WHERE admin = 1);
-- Audit current permissions
SELECT id, username, admin, modules 
FROM tl_user 
WHERE FIND_IN_SET('rss_reader', modules) > 0;
Mitigation architecture showing layered SSRF protections: HTTP client wrapper, URL validation, DCA restrictions, and permission controls
Mitigation architecture showing layered SSRF protections: HTTP client wrapper, URL validation, DCA restrictions, and permission controls

Long-Term Security Hardening

Network Segmentation Best Practices

1. Isolate Web Application Tier

┌─────────────────────────────────────────────────────┐
│ DMZ (Demilitarized Zone)                            │
│ ┌──────────────────────────────────────────────┐   │
│ │ Contao CMS Server                            │   │
│ │ - Limited outbound connectivity               │   │
│ │ - No direct access to internal networks       │   │
│ └──────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘
         ↓ (Restricted firewall rules)
┌─────────────────────────────────────────────────────┐
│ Internal Network Zone                               │
│ ┌──────────────────────────────────────────────┐   │
│ │ Database Servers (PostgreSQL, MySQL)         │   │
│ │ Internal APIs                                 │   │
│ │ Admin Panels                                  │   │
│ └──────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘

Firewall Rules:

# Allow Contao to internal database (specific host/port only)
Allow: Contao_Server → Database_Server:5432 (PostgreSQL)
Allow: Contao_Server → Database_Server:3306 (MySQL)
# Block all other internal network access
Deny: Contao_Server → 10.0.0.0/8
Deny: Contao_Server → 172.16.0.0/12
Deny: Contao_Server → 192.168.0.0/16
Deny: Contao_Server → 127.0.0.1/8
# Allow external feed sources only
Allow: Contao_Server → Internet:80 (HTTP)
Allow: Contao_Server → Internet:443 (HTTPS)

HTTP Client Configuration Standards

Establish Organization-Wide Standards:

<?php
// File: config/services.yaml
services:
  # Define secure HTTP client as default
  secure_http_client:
    class: Symfony\Component\HttpClient\NoPrivateNetworkHttpClient
    arguments:
      - '@http_client'
    public: true
  # Inject into all services requiring HTTP
  App\Service\FeedReader:
    arguments:
      $httpClient: '@secure_http_client'
  App\Service\WebhookHandler:
    arguments:
      $httpClient: '@secure_http_client'
  App\Service\ExternalApiClient:
    arguments:
      $httpClient: '@secure_http_client'

Configuration File:

# config/packages/http_client.yaml
framework:
  http_client:
    default_client: secure_client
    clients:
      secure_client:
        # Disable redirects to prevent SSRF via redirect chains
        max_redirects: 0
        # Set reasonable timeout
        timeout: 10
        # Verify SSL certificates
        verify_peer: true
        verify_host: true
        # Custom headers
        headers:
          User-Agent: 'Contao/4.x'

Input Validation Framework

Create Centralized Validation Service:

<?php
// File: src/Security/InputValidator.php
namespace App\Security;
class InputValidator
{
    private const ALLOWED_SCHEMES = ['http', 'https'];
    private const PRIVATE_RANGES = [
        '10.0.0.0/8',
        '172.16.0.0/12',
        '192.168.0.0/16',
        '127.0.0.0/8',
        '169.254.0.0/16',
    ];
    /**
     * Validate external URL
     */
    public static function validateExternalUrl(string $url): void
    {
        // 1. Parse URL
        $parsed = parse_url($url);
        if (!$parsed) {
            throw new \InvalidArgumentException('Invalid URL format');
        }
        // 2. Check scheme
        $scheme = $parsed['scheme'] ?? '';
        if (!in_array($scheme, self::ALLOWED_SCHEMES)) {
            throw new \InvalidArgumentException(
                "Scheme '$scheme' not allowed. Use HTTP or HTTPS."
            );
        }
        // 3. Get hostname
        $host = $parsed['host'] ?? '';
        if (empty($host)) {
            throw new \InvalidArgumentException('URL missing hostname');
        }
        // 4. Resolve hostname to IP
        $ip = @gethostbyname($host);
        if ($ip === $host) {
            // DNS resolution failed
            throw new \InvalidArgumentException("Cannot resolve hostname: $host");
        }
        // 5. Check against private ranges
        foreach (self::PRIVATE_RANGES as $range) {
            if (self::ipInRange($ip, $range)) {
                throw new \InvalidArgumentException(
                    "URL targets private network ($ip)"
                );
            }
        }
        // 6. Check for localhost variations
        if (in_array($host, ['localhost', 'localhost.localdomain', '::1'])) {
            throw new \InvalidArgumentException('Localhost not permitted');
        }
    }
    /**
     * Check if IP is within CIDR range
     */
    private static function ipInRange(string $ip, string $range): bool
    {
        [$subnet, $bits] = explode('/', $range);
        $ip = ip2long($ip);
        $subnet = ip2long($subnet);
        $mask = -1 << (32 - $bits);
        $subnet &= $mask;
        return ($ip & $mask) === $subnet;
    }
}

Security Testing and Code Review

Automated Security Testing:

#!/bin/bash
# File: scripts/security-test.sh
echo "Running SSRF vulnerability tests..."
# Test 1: Verify NoPrivateNetworkHttpClient is used
echo "[Test 1] Checking for SSRF protection..."
if grep -r "NoPrivateNetworkHttpClient" src/; then
    echo "✓ PASS: SSRF protection found"
else
    echo "✗ FAIL: SSRF protection not found"
    exit 1
fi
# Test 2: Verify URL validation is implemented
echo "[Test 2] Checking for URL validation..."
if grep -r "validateExternalUrl\|FeedUrlValidator" src/; then
    echo "✓ PASS: URL validation found"
else
    echo "✗ FAIL: URL validation not found"
    exit 1
fi
# Test 3: Run static analysis
echo "[Test 3] Running static analysis..."
vendor/bin/phpstan analyse src/ --level=8
# Test 4: Check for hardcoded credentials
echo "[Test 4] Checking for hardcoded credentials..."
if grep -r "password\|secret\|token" src/ | grep -v "// " | grep -v "validator"; then
    echo "⚠ WARNING: Possible hardcoded credentials found"
else
    echo "✓ PASS: No obvious hardcoded credentials"
fi
echo "Security tests completed"

Code Review Checklist:

## SSRF Security Code Review Checklist
### HTTP Client Usage
- [ ] All HTTP clients wrapped with NoPrivateNetworkHttpClient
- [ ] No direct use of cURL or stream functions for external requests
- [ ] Timeout values set (10-30 seconds)
- [ ] SSL certificate verification enabled
### Input Validation
- [ ] All user-supplied URLs validated before use
- [ ] URL scheme restricted to http/https
- [ ] Private IP ranges rejected
- [ ] Localhost variations rejected
- [ ] Validation happens before HTTP request
### Error Handling
- [ ] Network errors logged without exposing internal details
- [ ] User-facing errors don't reveal internal infrastructure
- [ ] Exceptions caught and handled appropriately
- [ ] No stack traces exposed to users
### Access Control
- [ ] Feed Reader module restricted to admin users
- [ ] Permission checks enforced at DCA level
- [ ] Role-based access control implemented
- [ ] Audit logging enabled for module access
### Testing
- [ ] Unit tests for URL validation
- [ ] Integration tests for HTTP client
- [ ] Negative tests for private IP ranges
- [ ] Penetration testing completed

Implementation Guide

Step-by-Step Mitigation Deployment

Phase 1: Preparation (Day 1)

# 1. Create backup of current installation
tar -czf /backup/contao-$(date +%Y%m%d-%H%M%S).tar.gz /var/www/contao/
# 2. Document current configuration
mysqldump -u root -p contao > /backup/contao-db-$(date +%Y%m%d).sql
# 3. List all users with Feed Reader access
mysql -u root -p -e "
SELECT u.id, u.username, u.admin, u.modules 
FROM tl_user u 
WHERE FIND_IN_SET('rss_reader', u.modules) > 0;
" contao > /tmp/feed-reader-users.txt
# 4. Audit current Feed Reader configurations
mysql -u root -p -e "
SELECT id, name, rss_feed, tstamp 
FROM tl_module 
WHERE type='rss_reader' 
ORDER BY tstamp DESC;
" contao > /tmp/feed-reader-config.txt

Phase 2: Code Deployment (Day 1-2)

# 1. Create feature branch
git checkout -b feature/ssrf-mitigation
# 2. Copy updated FeedReaderController.php
cp /path/to/patched/FeedReaderController.php \
   vendor/contao/core-bundle/src/Controller/
# 3. Add validation classes
cp /path/to/FeedUrlValidator.php src/Security/
cp /path/to/InputValidator.php src/Security/
cp /path/to/FeedUrlCallback.php src/Callbacks/
# 4. Update DCA configuration
cp /path/to/tl_module.php contao/dca/
# 5. Update services configuration
cp /path/to/services.yaml config/
# 6. Clear Contao cache
php bin/console cache:clear --env=prod
# 7. Run tests
vendor/bin/phpunit tests/Security/FeedUrlValidatorTest.php
vendor/bin/phpunit tests/Controller/FeedReaderControllerTest.php

Phase 3: Testing (Day 2-3)

# 1. Unit tests for URL validation
php -r "
require 'vendor/autoload.php';
require 'src/Security/FeedUrlValidator.php';
\$tests = [
    'https://example.com/feed.xml' => true,
    'http://192.168.1.1/feed' => false,
    'http://localhost/feed' => false,
    'http://169.254.169.254/metadata' => false,
];
foreach (\$tests as \$url => \$shouldPass) {
    try {
        FeedUrlValidator::validate(\$url);
        \$result = 'PASS';
    } catch (Exception \$e) {
        \$result = 'FAIL';
    }
    \$expected = \$shouldPass ? 'PASS' : 'FAIL';
    \$status = \$result === \$expected ? '✓' : '✗';
    echo \"\$status \$url: \$result (expected \$expected)
\";
}
"
# 2. Integration test with HTTP client
php bin/console test:feed-reader-ssrf
# 3. Manual testing in staging environment
# - Test valid external feed URLs
# - Attempt to access private IP ranges (should fail)
# - Verify error messages don't expose internal details

Testing and Validation Procedures

Functional Testing:

“`php
<?php
// File: tests/Controller/FeedReaderControllerTest.php

namespace App\Tests\Controller;

use PHPUnit\Framework\TestCase;
use App\Controller\FeedReaderController;
use App\Security\FeedUrlValidator;

class FeedReaderController

← Back to IT Security