DEV Community

Cover image for How to Create Safe External Redirects in WordPress
anti Gym Club
anti Gym Club

Posted on • Originally published at antigymclub.com

How to Create Safe External Redirects in WordPress

You want to redirect a WordPress post to an external URL. Maybe it's an affiliate link, a resource that moved, or a curated link collection.

The naive approach using wp_redirect() with any URL opens security holes. Here's how to do it safely.

The Problem with wp_redirect()

// DON'T DO THIS
wp_redirect( $_GET['url'] );
exit;
Enter fullscreen mode Exit fullscreen mode

This is an open redirect vulnerability. Attackers can craft URLs like:

yoursite.com/post?url=https://malicious-site.com
Enter fullscreen mode Exit fullscreen mode

And use your domain's reputation to phish users.

Safe External Redirects

WordPress provides wp_safe_redirect(), but it only allows redirects to whitelisted hosts. For external URLs, we need validation:

/**
 * Safely redirect to an external URL
 *
 * @param string $url         Destination URL
 * @param int    $status_code HTTP status code (301, 302, 307)
 */
function safe_external_redirect( $url, $status_code = 301 ) {
    // Validate URL format
    if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
        wp_die( 'Invalid redirect URL' );
    }

    // Only allow http/https protocols
    $allowed_protocols = array( 'http', 'https' );
    $parsed = wp_parse_url( $url );

    if ( ! isset( $parsed['scheme'] ) ||
         ! in_array( strtolower( $parsed['scheme'] ), $allowed_protocols, true ) ) {
        wp_die( 'Invalid URL protocol' );
    }

    // Validate status code
    $valid_codes = array( 301, 302, 307 );
    if ( ! in_array( $status_code, $valid_codes, true ) ) {
        $status_code = 301;
    }

    // Perform redirect
    wp_redirect( esc_url_raw( $url ), $status_code );
    exit;
}
Enter fullscreen mode Exit fullscreen mode

Building a Post-Based Redirect System

Let's create a system where editors can set an external redirect URL per post.

Step 1: Add Meta Box

/**
 * Add redirect meta box
 */
function add_redirect_meta_box() {
    add_meta_box(
        'external_redirect_box',
        'External Redirect',
        'render_redirect_meta_box',
        'post',
        'side',
        'high'
    );
}
add_action( 'add_meta_boxes', 'add_redirect_meta_box' );

/**
 * Render meta box
 */
function render_redirect_meta_box( $post ) {
    $enabled = get_post_meta( $post->ID, '_redirect_enabled', true );
    $url = get_post_meta( $post->ID, '_redirect_url', true );

    wp_nonce_field( 'redirect_meta_nonce', 'redirect_nonce' );
    ?>
    <p>
        <label>
            <input type="checkbox" name="redirect_enabled" value="1"
                <?php checked( $enabled, '1' ); ?>>
            Enable external redirect
        </label>
    </p>
    <p>
        <label for="redirect_url">Destination URL:</label>
        <input type="url" name="redirect_url" id="redirect_url"
            class="widefat" placeholder="https://example.com"
            value="<?php echo esc_attr( $url ); ?>">
    </p>
    <p class="description">
        Visitors will be redirected to this URL when viewing this post.
    </p>
    <?php
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Save with Validation

/**
 * Save redirect meta
 */
function save_redirect_meta( $post_id ) {
    // Verify nonce
    if ( ! isset( $_POST['redirect_nonce'] ) ||
         ! wp_verify_nonce( $_POST['redirect_nonce'], 'redirect_meta_nonce' ) ) {
        return;
    }

    // Check permissions
    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }

    // Skip autosave
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }

    // Save enabled state
    $enabled = isset( $_POST['redirect_enabled'] ) ? '1' : '0';
    update_post_meta( $post_id, '_redirect_enabled', $enabled );

    // Validate and save URL
    $url = '';
    if ( isset( $_POST['redirect_url'] ) ) {
        $url = esc_url_raw( wp_unslash( $_POST['redirect_url'] ) );

        // Double-check it's a valid URL
        if ( ! empty( $url ) && ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
            // Store error for admin notice
            set_transient( 'redirect_error_' . $post_id, 'invalid_url', 30 );
            return;
        }
    }

    update_post_meta( $post_id, '_redirect_url', $url );
}
add_action( 'save_post', 'save_redirect_meta' );
Enter fullscreen mode Exit fullscreen mode

Step 3: Handle Frontend Redirect

/**
 * Handle redirect on frontend
 */
function handle_external_redirect() {
    // Only on single posts
    if ( ! is_singular( 'post' ) || ! is_main_query() ) {
        return;
    }

    $post_id = get_the_ID();
    $enabled = get_post_meta( $post_id, '_redirect_enabled', true );
    $url = get_post_meta( $post_id, '_redirect_url', true );

    if ( '1' !== $enabled || empty( $url ) ) {
        return;
    }

    // Allow admins to bypass for testing
    if ( current_user_can( 'edit_posts' ) && isset( $_GET['no_redirect'] ) ) {
        return;
    }

    // Validate URL before redirecting
    if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
        return;
    }

    // Redirect with 301 (permanent)
    wp_redirect( esc_url_raw( $url ), 301 );
    exit;
}
add_action( 'template_redirect', 'handle_external_redirect' );
Enter fullscreen mode Exit fullscreen mode

Choosing the Right Status Code

Code Name When to Use
301 Permanent The old URL will never be used again. SEO value transfers.
302 Found Temporary redirect. Original URL might come back.
307 Temporary Like 302, but preserves HTTP method (POST stays POST).

For most external redirects, 301 is correct. It tells search engines to update their index.

Modifying Permalinks in Listings

When a post has an external redirect, you might want links in menus and archives to point directly to the external URL:

/**
 * Modify permalink for redirected posts
 */
function modify_redirected_permalink( $permalink, $post ) {
    // Don't modify in admin
    if ( is_admin() ) {
        return $permalink;
    }

    $enabled = get_post_meta( $post->ID, '_redirect_enabled', true );
    $url = get_post_meta( $post->ID, '_redirect_url', true );

    if ( '1' === $enabled && ! empty( $url ) && filter_var( $url, FILTER_VALIDATE_URL ) ) {
        return $url;
    }

    return $permalink;
}
add_filter( 'post_link', 'modify_redirected_permalink', 10, 2 );
Enter fullscreen mode Exit fullscreen mode

Adding Admin Bar Link for Testing

Editors need to view the actual post (not get redirected) when testing:

/**
 * Add "View Without Redirect" to admin bar
 */
function add_bypass_link( $wp_admin_bar ) {
    if ( ! is_singular( 'post' ) || ! current_user_can( 'edit_posts' ) ) {
        return;
    }

    $post_id = get_the_ID();
    $enabled = get_post_meta( $post_id, '_redirect_enabled', true );

    if ( '1' !== $enabled ) {
        return;
    }

    $wp_admin_bar->add_node( array(
        'id'    => 'view-no-redirect',
        'title' => 'View Without Redirect',
        'href'  => add_query_arg( 'no_redirect', '1', get_permalink() ),
        'meta'  => array( 'target' => '_blank' ),
    ) );
}
add_action( 'admin_bar_menu', 'add_bypass_link', 999 );
Enter fullscreen mode Exit fullscreen mode

Security Checklist

  1. Always validate URLs with filter_var( $url, FILTER_VALIDATE_URL )
  2. Sanitize on save with esc_url_raw()
  3. Escape on output with esc_url()
  4. Check protocols only allow http/https
  5. Use nonces when saving meta
  6. Verify capabilities only editors should set redirects
  7. Never redirect to user input without validation

Common Issues

Redirect loops:
Make sure the redirect URL isn't the post's own URL. Add this check:

$current_url = get_permalink( $post_id );
if ( trailingslashit( $url ) === trailingslashit( $current_url ) ) {
    return; // Don't redirect to self
}
Enter fullscreen mode Exit fullscreen mode

Cached redirects:
If you change a redirect and it doesn't update, clear your site cache and browser cache. 301 redirects are aggressively cached.

SEO implications:
301 redirects pass ~90% of link equity. This is fine for legitimate use cases. But don't use redirects to manipulate search rankings that's against Google's guidelines.


If you want a complete solution with a settings page, redirect logging, and support for pages/CPTs, check out External Redirect Pro.

Resources:

Top comments (0)