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:
- Attacker Authentication: Threat actor logs into Contao backend with valid credentials
- Module Access: Navigates to Feed Reader module configuration
- 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)
- Request Execution: Feed Reader module processes the URL without validation
- Response Retrieval: Server makes HTTP request from its network context
- 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:
- Configure Feed Reader with URLs targeting common internal IP ranges
- Observe response times and error messages
- Map active services:
http://192.168.1.1/,http://192.168.1.50:5432/, etc. - 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:
- Configure Feed Reader URL:
http://internal-db.local:5432/ - Observe database connection responses
- Attempt to extract data through error-based techniques
- 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:
- Configure Feed Reader URL:
http://169.254.169.254/latest/meta-data/iam/security-credentials/ - Retrieve IAM role name from metadata service
- Access credentials endpoint:
http://169.254.169.254/latest/meta-data/iam/security-credentials/[ROLE-NAME] - 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:
- Establish initial access through SSRF vulnerability
- Enumerate internal services and identify high-value targets
- Use Contao server to probe security controls and firewall rules
- Pivot to domain controller, backup systems, or payment processing infrastructure
- 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;

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