Introduction
Open redirect vulnerabilities rank among the most underestimated security risks in WordPress plugins. They don't create the dramatic compromises of SQL injection or remote code execution—the attack is subtle, often invisible to site owners. Yet open redirects enable sophisticated phishing attacks, credential harvesting, malware distribution, and account hijacking.
An open redirect occurs when your plugin accepts a URL parameter and redirects to it without validation. An attacker crafts a link to your site that redirects to a malicious domain: https://yoursite.com/?redirect=https://attacker.com/phishing. Users trust the domain in the URL bar and click. The site redirects them to a convincing phishing page. By the time they realize they're on a different domain, their credentials are already stolen.
The devastating part: your domain's reputation lends credibility to the attack. Users who see yoursite.com in the URL bar believe they're safe. The redirect to attacker.com happens in a split second, and many don't even notice.
WordPress provides wp_safe_redirect() to prevent these attacks, yet many plugins still use the dangerous wp_redirect() without validation. Understanding the difference between these functions, implementing proper URL validation, detecting redirect chain attacks, and using the allowed_redirect_hosts filter is essential for secure plugin development.
This guide explores every aspect of WordPress open redirect security, from the vulnerability mechanics to production-ready defense patterns that WP HealthKit's automated audits identify before they reach users.
Table of Contents
- Understanding Open Redirect Vulnerabilities
- wp_redirect vs wp_safe_redirect
- URL Validation and Whitelist Approaches
- The allowed_redirect_hosts Filter
- Preventing Redirect Chain Attacks
- Dynamic Redirect Handling
- Testing Redirect Security
- Frequently Asked Questions
Understanding Open Redirect Vulnerabilities
An open redirect vulnerability is a case where a plugin redirects users to a URL provided by an attacker without proper validation. The impact extends far beyond an inconvenient redirect. Open redirects are insidious because they abuse user trust in your domain. Users see your domain in the URL bar and assume they're on a safe, legitimate website. When a redirect happens in the background, many users don't even notice they've moved to a different domain. The redirect is so fast that browsers might not show the transition. By the time users realize they're on a different site—usually when they're prompted to re-enter credentials on what they think is the real site—attackers have already captured what they need.
Why Open Redirects Matter More Than Code Suggests
Many developers dismiss open redirects as low-severity vulnerabilities. "It's just a redirect," they think. "Users could navigate to bad sites anyway without my help." However, security researchers and attackers understand that redirects from trusted domains are incredibly effective attack tools. A direct link to a phishing site gets caught by email filters, browser warnings, and user skepticism. A link from a legitimate domain that redirects to a phishing site bypasses all of these defenses. The attacking infrastructure gains legitimacy by association with your trusted domain. In the few milliseconds of the redirect, users' brains don't have time to process that they've changed domains. By the time they're asked to enter credentials, their default assumption is that they're on the real site. This psychological aspect makes open redirects effective at a scale that direct phishing links never achieve.
// VULNERABLE: Don't do this
function my_plugin_redirect() {
if ( isset( $_GET['redirect'] ) ) {
wp_redirect( $_GET['redirect'] );
exit;
}
}
// Attack vector:
// https://example.com/?redirect=https://attacker.com/steal-credentials
// Users see example.com in the URL, trust it, and click
// They're redirected to attacker.com before they notice
Attackers exploit open redirects in multiple ways:
Phishing Attacks: Create a fake login page on their domain that looks identical to the real site. When users are redirected there from your legitimate domain, they trust the redirect and enter credentials.
Malware Distribution: Redirect users to sites hosting malware. The fact that the initial redirect came from a trusted domain increases the likelihood of infection.
Credential Harvesting: Create pages that mimic the target site's login screen, collecting credentials that the attacker then uses to access real accounts.
Account Hijacking: If the redirect target is a password reset page on another service, attackers can hijack accounts by controlling where password reset links redirect.
Reputation Damage: Your domain gets associated with phishing and malware distribution, damaging user trust and potentially triggering search engine penalties.
The OWASP Unvalidated Redirects guide details these attacks with real-world examples. The defense is straightforward but requires discipline: never redirect to user-provided URLs without validation.
wp_redirect vs wp_safe_redirect
WordPress provides two redirect functions with different security levels.
// wp_redirect: No validation
wp_redirect( $url );
// Redirects to any URL without checking
// wp_safe_redirect: Validates the URL is internal
wp_safe_redirect( $url );
// Only redirects to internal URLs by default
wp_safe_redirect() uses the allowed_redirect_hosts filter to determine which hosts are safe for redirect. By default, it only allows redirects to:
- Your own domain
- Subdomains of your domain
- Other domains explicitly added via the filter
// Using wp_safe_redirect is the standard approach
if ( isset( $_GET['redirect'] ) ) {
// This is safer - only internal redirects allowed
wp_safe_redirect( $_GET['redirect'] );
exit;
}
// However, you should still validate the URL
if ( isset( $_GET['redirect'] ) ) {
$redirect = $_GET['redirect'];
// Additional validation
if ( ! preg_match( '#^https?://#', $redirect ) ) {
// Relative URL - safe
wp_safe_redirect( $redirect );
exit;
}
// Absolute URL - verify it's your domain
if ( wp_http_validate_url( $redirect ) ) {
wp_safe_redirect( $redirect );
exit;
}
// Invalid or external URL - redirect to home
wp_safe_redirect( home_url() );
exit;
}
The key difference is that wp_safe_redirect() provides a default security level and respects the allowed_redirect_hosts filter, allowing site administrators to configure what's acceptable.
// Under the hood, wp_safe_redirect checks:
$allowed_hosts = apply_filters( 'allowed_redirect_hosts', array( parse_url( home_url(), PHP_URL_HOST ) ) );
if ( in_array( parse_url( $url, PHP_URL_HOST ), $allowed_hosts, true ) ) {
wp_redirect( $url );
} else {
wp_safe_redirect( home_url() );
}
Always use wp_safe_redirect() unless you have a specific reason not to. And if you do use wp_redirect(), you must implement your own comprehensive validation.
The Security Philosophy Behind wp_safe_redirect()
Understanding the design philosophy of wp_safe_redirect() helps you use it correctly. WordPress recognizes that plugins need to redirect to external sites sometimes—to third-party services, payment gateways, OAuth providers, and affiliate links. Rather than making this impossible, WordPress allows it through the allowed_redirect_hosts filter, which gives site administrators visibility and control. If a plugin is redirecting somewhere, administrators should know about it and be able to trust it. This is better than plugins secretly redirecting to unknown places. However, by default, wp_safe_redirect() blocks external redirects unless explicitly allowed. This "default deny" approach provides security for sites whose administrators haven't explicitly allowed external redirects. A plugin that uses wp_safe_redirect() correctly respects this default deny behavior and registers any external hosts it legitimately needs through the filter. This approach balances security with functionality.
When wp_redirect() Might Be Necessary
There are rare cases where you might need to use wp_redirect() directly with comprehensive validation:
- Redirecting to third-party OAuth providers for authentication
- Integrating with external payment processors
- Implementing single sign-on with external systems
- Redirecting to CDN-hosted resources for downloads
In these cases, implement validation that checks the URL structure, domain whitelist, and protocol. Never rely on wp_redirect() alone.
URL Validation and Whitelist Approaches
When you need to redirect to external URLs, implement strict whitelisting rather than blacklisting.
// INSECURE: Blacklist approach - easy to bypass
function insecure_redirect( $redirect_url ) {
// Try to block bad domains
$blocked = array( 'attacker.com', 'malware.com' );
foreach ( $blocked as $domain ) {
if ( false !== strpos( $redirect_url, $domain ) ) {
wp_safe_redirect( home_url() );
exit;
}
}
// Attacker can use typos, subdomains, etc. to bypass
wp_redirect( $redirect_url );
exit;
}
// SECURE: Whitelist approach - only allow known good URLs
function secure_redirect( $redirect_url ) {
$allowed_urls = array(
'https://example.com/page1',
'https://example.com/page2',
'https://trusted-partner.com/integration',
);
if ( in_array( $redirect_url, $allowed_urls, true ) ) {
wp_redirect( $redirect_url );
exit;
}
// If not whitelisted, go to home
wp_safe_redirect( home_url() );
exit;
}
Whitelisting is more restrictive but dramatically more secure. Instead of trying to identify all bad URLs (impossible), you explicitly allow only known good URLs.
For plugin settings, store allowed redirect destinations in your plugin options:
// Store allowed redirect URLs in plugin settings
function my_plugin_set_redirect_pages() {
$redirect_pages = array();
// Admin can configure allowed redirect pages
if ( isset( $_POST['my_plugin_redirect_pages'] ) ) {
$redirect_pages = array_map( 'sanitize_text_field', $_POST['my_plugin_redirect_pages'] );
update_option( 'my_plugin_redirect_pages', $redirect_pages );
}
}
// Use whitelisted URLs
function my_plugin_do_redirect() {
if ( isset( $_GET['redirect'] ) ) {
$redirect = sanitize_text_field( $_GET['redirect'] );
$allowed = get_option( 'my_plugin_redirect_pages', array() );
if ( in_array( $redirect, $allowed, true ) ) {
wp_safe_redirect( $redirect );
exit;
}
}
wp_safe_redirect( home_url() );
exit;
}
Another approach is to use numeric IDs and look up the actual URL:
// Map numeric IDs to URLs - user can't specify arbitrary URLs
function my_plugin_redirect_by_id() {
if ( ! isset( $_GET['redirect_id'] ) ) {
return;
}
$redirect_id = (int) $_GET['redirect_id'];
// Define redirect mappings
$redirects = array(
1 => 'https://example.com/page1',
2 => 'https://example.com/page2',
3 => 'https://trusted-partner.com/integration',
);
if ( isset( $redirects[ $redirect_id ] ) ) {
wp_safe_redirect( $redirects[ $redirect_id ] );
exit;
}
}
This approach is extremely secure because users can't control the target URL directly—they can only choose from predefined options.
The allowed_redirect_hosts Filter
The allowed_redirect_hosts filter lets site administrators specify which hosts are safe for redirects. Understanding this filter helps you work with site security policies. This filter is a security boundary between plugins and administrators. Plugins declare where they need to redirect, and administrators review and control what's allowed. It's part of WordPress's broader approach to defense in depth—no single component is trusted completely, but security comes from multiple layers checking each other.
How the Filter Works in Practice
The allowed_redirect_hosts filter starts with just your site's domain. Any plugin that needs external redirects adds to this list. When wp_safe_redirect() is called, it checks if the target host is in the allowed list. If not, it redirects to home_url() instead. This simple mechanism provides transparency—administrators can audit all redirect destinations by checking what's in the filter, and they can remove any they don't trust. If a plugin is redirecting to suspicious domains, administrators can disable those redirects by removing them from the filter. Some sites take this further, hooking the filter in their theme or a must-use plugin to add additional security policy. For example, a site might add all its regional domains to the whitelist while blocking everything else, ensuring redirects never go off-domain unintentionally.
// WordPress core uses this filter
$allowed_hosts = apply_filters( 'allowed_redirect_hosts', array( $default_host ) );
// Site administrators can extend it
add_filter( 'allowed_redirect_hosts', function( $hosts ) {
$hosts[] = 'trusted-partner.com';
$hosts[] = 'affiliate.example.com';
return $hosts;
} );
// Your plugin can also add to it
function my_plugin_register_allowed_hosts( $hosts ) {
// If your plugin has legitimate external redirects
$hosts[] = 'official-cdn.example.com';
return $hosts;
}
add_filter( 'allowed_redirect_hosts', 'my_plugin_register_allowed_hosts' );
If your plugin needs to redirect to specific external sites as part of its functionality, register those hosts with the filter:
// Register external hosts your plugin legitimately needs
add_action( 'plugins_loaded', function() {
if ( has_filter( 'allowed_redirect_hosts' ) ) {
add_filter( 'allowed_redirect_hosts', function( $hosts ) {
// Your plugin's legitimate redirect destinations
$hosts[] = 'payment-gateway.example.com';
$hosts[] = 'external-service.example.com';
return $hosts;
} );
}
} );
This approach gives administrators visibility into and control over where your plugin redirects users.
Preventing Redirect Chain Attacks
Redirect chains (redirects that themselves redirect) can be exploited to bypass security checks.
// VULNERABLE: Doesn't prevent redirect chains
function vulnerable_redirect( $user_url ) {
if ( wp_http_validate_url( $user_url ) ) {
wp_safe_redirect( $user_url );
exit;
}
}
// Attack vector:
// 1. Attacker finds your site redirects to: trusted-domain.com/redirect?url=user_input
// 2. trusted-domain.com's redirect is vulnerable too
// 3. Attacker chains: yoursite.com → trusted-domain.com → attacker.com
// 4. If yoursite.com is whitelisted, the chain bypasses security
Prevent redirect chains by:
- Never including a URL in a redirect parameter: Use IDs instead
- Validating the final destination: Check where a redirect actually goes
- Limiting redirect depth: Don't follow chains more than 1 level
- Sanitizing redirect URLs: Only allow URLs matching specific patterns
// Secure: Check where a URL actually redirects to
function safe_redirect_with_chain_detection( $initial_url ) {
$url = $initial_url;
$redirected_urls = array();
// Prevent infinite loops
for ( $i = 0; $i < 3; $i++ ) {
// Get the target of this URL
$response = wp_remote_head( $url, array( 'redirection' => 0 ) );
if ( is_wp_error( $response ) ) {
// Network error - don't redirect
wp_safe_redirect( home_url() );
exit;
}
$status = (int) wp_remote_retrieve_response_code( $response );
// Check if it's a redirect
if ( in_array( $status, array( 301, 302, 303, 307, 308 ), true ) ) {
$location = wp_remote_retrieve_header( $response, 'location' );
if ( ! $location ) {
break;
}
$redirected_urls[] = $url;
$url = $location;
} else {
// Not a redirect, this is our final destination
break;
}
}
// Verify final URL is safe
if ( in_array( wp_http_validate_url( $url ), array( 'http', 'https' ), true ) ) {
// Check against whitelist
$allowed = get_option( 'my_plugin_allowed_urls', array() );
if ( in_array( $url, $allowed, true ) ) {
wp_safe_redirect( $url );
exit;
}
}
wp_safe_redirect( home_url() );
exit;
}
However, following redirects to check their destination can be time-consuming. A better approach is to avoid the problem entirely by using IDs and whitelists.
Mid-Article CTA
Open redirect vulnerabilities are subtle but dangerous. They enable phishing attacks, credential harvesting, and malware distribution—all while appearing to come from your legitimate domain. Is your plugin handling redirects safely?
WP HealthKit audits your plugin code to identify open redirect vulnerabilities before they reach users. Our security scans detect unsafe redirect usage, missing URL validation, and redirect chain risks that standard code review might miss.
Scan Your Plugin's Redirect Security with WP HealthKit and ensure every redirect is protected against open redirect attacks.
Dynamic Redirect Handling
Sometimes plugins need to redirect based on complex logic. Handle this safely by validating at every step.
// Safe handling of dynamic redirects
function my_plugin_dynamic_redirect() {
// Only process POST requests from authenticated users
if ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
return;
}
if ( ! is_user_logged_in() ) {
wp_safe_redirect( wp_login_url() );
exit;
}
// Verify nonce
if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'my_plugin_redirect' ) ) {
wp_safe_redirect( home_url() );
exit;
}
// Get action to determine redirect
$action = isset( $_POST['action'] ) ? sanitize_text_field( $_POST['action'] ) : '';
// Map actions to safe redirects
$redirect_map = array(
'save_and_continue' => add_query_arg( 'status', 'saved', wp_get_referer() ),
'save_and_exit' => admin_url( 'admin.php?page=my-plugin' ),
'publish' => get_permalink( $_POST['post_id'] ?? 0 ),
);
if ( isset( $redirect_map[ $action ] ) ) {
$redirect = $redirect_map[ $action ];
// Validate the redirect URL
if ( wp_http_validate_url( $redirect ) ) {
wp_safe_redirect( $redirect );
exit;
}
}
// Default safe fallback
wp_safe_redirect( home_url() );
exit;
}
add_action( 'admin_init', 'my_plugin_dynamic_redirect' );
Key practices:
- Only process POST requests for state-changing redirects
- Verify authentication and nonces
- Use whitelisted action-to-URL mappings
- Validate all redirects with
wp_http_validate_url() - Provide a safe default fallback
Testing Redirect Security
Test that your plugin's redirects are secure against open redirect attacks.
<?php
/**
* Test redirect security
*/
class Test_Redirect_Security extends WP_UnitTestCase {
public function test_blocks_external_redirect() {
// Attempt redirect to external site
$_GET['redirect'] = 'https://attacker.com/phishing';
// Capture redirect
try {
my_plugin_redirect();
} catch ( Exception $e ) {
// Redirects throw exceptions in tests
}
// Verify safe redirect was used instead
$this->assertStringContainsString( home_url(), $this->last_redirect );
}
public function test_allows_internal_redirect() {
$internal_url = home_url( '/about/' );
$_GET['redirect'] = $internal_url;
try {
my_plugin_redirect();
} catch ( Exception $e ) {
// Expected
}
$this->assertStringContainsString( '/about/', $this->last_redirect );
}
public function test_blocks_protocol_confusion() {
// JavaScript: protocol is dangerous
$_GET['redirect'] = 'javascript:alert("xss")';
try {
my_plugin_redirect();
} catch ( Exception $e ) {
// Expected
}
$this->assertStringContainsString( home_url(), $this->last_redirect );
}
public function test_blocks_data_urls() {
$_GET['redirect'] = 'data:text/html,<script>alert("xss")</script>';
try {
my_plugin_redirect();
} catch ( Exception $e ) {
// Expected
}
$this->assertStringContainsString( home_url(), $this->last_redirect );
}
public function test_validates_whitelisted_urls() {
update_option( 'my_plugin_allowed_urls', array(
'https://trusted-partner.com/callback',
) );
$_GET['redirect'] = 'https://trusted-partner.com/callback';
try {
my_plugin_redirect();
} catch ( Exception $e ) {
// Expected
}
$this->assertStringContainsString( 'trusted-partner.com', $this->last_redirect );
}
}
Test your redirects against:
- External URLs
- JavaScript protocol redirects
- Data URL schemes
- Relative paths
- Query parameter injection
- Multiple levels of encoding
Additional Resources
For a comprehensive view of how WP HealthKit approaches plugin analysis, explore our 17 verification layers or browse the plugin directory to see real audit scores. Ready to check your own plugin? Run a free audit now.
Frequently Asked Questions
Is wp_safe_redirect always secure enough?
wp_safe_redirect() provides good default protection by blocking external redirects. However, you should still validate the URL and consider using whitelists for critical redirects. Defense in depth is better than relying on a single function.
What if I need to redirect to an external URL?
Use the allowed_redirect_hosts filter to register external hosts your plugin legitimately needs. Better yet, use numeric IDs that map to predefined URLs so users can't control the target directly.
Can attackers bypass wp_safe_redirect?
Not easily, but they might find weaknesses in your URL validation. Always pair wp_safe_redirect() with additional validation like wp_http_validate_url() and whitelisting for critical operations.
Are relative URLs safe to redirect to?
Relative URLs are safer than absolute URLs to external domains, but they can still be exploited. For example, /../../../attacker.com might be interpreted differently by different browsers. Always validate carefully.
How do I handle login redirects safely?
Use wp_safe_redirect() for post-login redirects. Store the redirect URL in user settings rather than URL parameters when possible. If accepting URL parameters, use wp_login_url() or admin_url() as safe defaults.
Can WP HealthKit detect redirect vulnerabilities in my code?
Yes. WP HealthKit audits your plugin code to identify unsafe redirect patterns like direct wp_redirect() calls with user input, missing URL validation, and bypass vulnerabilities. It catches redirect security issues that manual review might miss.
Conclusion
Open redirect vulnerabilities are subtle yet dangerous. They don't require technical complexity to exploit—just a crafted link and user trust. By the time users realize they're on a different domain, attackers have already stolen credentials, distributed malware, or harvested personal information.
The defense is straightforward but requires discipline: always use wp_safe_redirect(), implement URL validation, use whitelisting instead of blacklisting, and register external domains via the allowed_redirect_hosts filter. Never assume user input is safe, and always provide a safe fallback redirect.
Your plugin's redirects directly impact user security. When you handle redirects carefully, you protect users from sophisticated phishing attacks that exploit your site's legitimate reputation.
Audit your plugin's redirect security with WP HealthKit to ensure every redirect is protected against open redirect attacks. Our security scans identify unsafe redirect patterns before they reach production and expose your users to attack.