Securing WordPress for Podcast Sites: Protecting Media and Feeds
Protect your WordPress podcast website with strategies to secure media files, RSS feeds, premium content, and listener data.
Podcast websites face unique security challenges including media hotlinking, feed hijacking, and premium content protection. Implementing proper security measures protects your content and revenue.
Podcast Site Security Challenges
Podcast sites face specific threats:
- Media file hotlinking and theft
- RSS feed hijacking attempts
- Premium content bypass
- Download statistics manipulation
- Subscriber data exposure
Content Protection Needs
Podcast sites must protect:
- Audio and video files
- RSS feed integrity
- Subscriber-only episodes
- Download analytics
- Member payment information
Preventing Media Hotlinking
Block unauthorized embedding of your media files:
// .htaccess hotlink protection for audio files
RewriteEngine on
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https://(www\.)?yourdomain\.com [NC]
RewriteCond %{HTTP_REFERER} !^https://player\.yourdomain\.com [NC]
RewriteCond %{HTTP_REFERER} !^https://(www\.)?podcatchers\.com [NC]
RewriteRule \.(mp3|mp4|m4a|ogg|wav)$ - [F,L]
PHP-based hotlink protection:
function serve_protected_audio($episode_id) {
$episode = get_episode($episode_id);
if (!$episode) {
wp_die('Episode not found', 404);
}
// Verify referer or signed URL
$valid_referers = array(
home_url(),
'apple.com',
'spotify.com',
'google.com',
'overcast.fm',
);
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$allowed = false;
foreach ($valid_referers as $valid) {
if (strpos($referer, $valid) !== false) {
$allowed = true;
break;
}
}
// Check for signed URL
if (!$allowed && !verify_signed_url($_GET)) {
wp_die('Access denied', 403);
}
// Track download
log_episode_download($episode_id, $_SERVER['REMOTE_ADDR']);
// Serve file
$filepath = $episode['file_path'];
header('Content-Type: audio/mpeg');
header('Content-Length: ' . filesize($filepath));
header('Accept-Ranges: bytes');
readfile($filepath);
exit;
}
Signed URL Generation
Create time-limited download URLs:
function generate_signed_media_url($episode_id, $user_id = null) {
$expires = time() + 3600; // 1 hour validity
$data = array(
'episode' => $episode_id,
'user' => $user_id,
'expires' => $expires,
);
$signature = hash_hmac('sha256', json_encode($data), wp_salt('auth'));
return add_query_arg(array(
'episode' => $episode_id,
'expires' => $expires,
'sig' => $signature,
), home_url('/podcast/download/'));
}
function verify_signed_url($params) {
if (empty($params['sig']) || empty($params['expires'])) {
return false;
}
// Check expiration
if ($params['expires'] < time()) {
return false;
}
$data = array(
'episode' => $params['episode'] ?? null,
'user' => $params['user'] ?? null,
'expires' => $params['expires'],
);
$expected_sig = hash_hmac('sha256', json_encode($data), wp_salt('auth'));
return hash_equals($expected_sig, $params['sig']);
}
RSS Feed Security
Protect your podcast RSS feed:
// Add authentication to premium feed
function generate_private_feed_url($user_id) {
$token = bin2hex(random_bytes(16));
// Store token
update_user_meta($user_id, 'podcast_feed_token', $token);
return add_query_arg(array(
'feed' => 'podcast',
'token' => $token,
), home_url());
}
// Validate feed access
add_action('do_feed_podcast', function() {
$token = $_GET['token'] ?? '';
if (empty($token)) {
// Public feed - show free episodes only
query_posts(array(
'post_type' => 'episode',
'meta_query' => array(
array(
'key' => '_premium',
'compare' => 'NOT EXISTS',
),
),
));
return;
}
// Validate token
global $wpdb;
$user_id = $wpdb->get_var($wpdb->prepare(
"SELECT user_id FROM {$wpdb->usermeta}
WHERE meta_key = 'podcast_feed_token' AND meta_value = %s",
$token
));
if (!$user_id || !user_has_active_subscription($user_id)) {
wp_die('Invalid or expired feed token');
}
// Show all episodes for valid subscribers
}, 1);
Premium Content Protection
Restrict access to subscriber-only episodes:
function check_episode_access($episode_id) {
$episode = get_post($episode_id);
if (!$episode || $episode->post_type !== 'episode') {
return false;
}
// Public episodes
if (!get_post_meta($episode_id, '_premium', true)) {
return true;
}
// Check user subscription
if (!is_user_logged_in()) {
return false;
}
$user_id = get_current_user_id();
// Check active subscription
if (user_has_active_subscription($user_id)) {
return true;
}
// Check individual episode purchase
if (user_purchased_episode($user_id, $episode_id)) {
return true;
}
return false;
}
// Protect episode page
add_action('template_redirect', function() {
if (!is_singular('episode')) {
return;
}
if (!check_episode_access(get_the_ID())) {
wp_redirect(home_url('/subscribe/'));
exit;
}
});
Download Analytics Protection
Prevent download count manipulation:
function log_episode_download($episode_id, $ip) {
global $wpdb;
// Check for recent download from same IP
$recent = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM podcast_downloads
WHERE episode_id = %d AND ip_address = %s
AND downloaded_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)",
$episode_id, $ip
));
if ($recent > 0) {
// Already counted, do not increment
return;
}
// Log download
$wpdb->insert('podcast_downloads', array(
'episode_id' => $episode_id,
'ip_address' => $ip,
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown',
'referer' => $_SERVER['HTTP_REFERER'] ?? '',
'downloaded_at' => current_time('mysql'),
));
// Update episode download count
$current = get_post_meta($episode_id, '_download_count', true) ?: 0;
update_post_meta($episode_id, '_download_count', $current + 1);
}
// Detect bot traffic
function is_podcast_bot($user_agent) {
$bots = array(
'Googlebot',
'bingbot',
'Baiduspider',
'YandexBot',
'facebookexternalhit',
);
foreach ($bots as $bot) {
if (stripos($user_agent, $bot) !== false) {
return true;
}
}
return false;
}
Subscriber Data Protection
Secure subscriber information:
function export_subscriber_data($user_id) {
// Verify request is from the user themselves
if (get_current_user_id() !== $user_id && !current_user_can('manage_options')) {
return new WP_Error('unauthorized', 'Cannot export other user data');
}
$data = array(
'email' => get_user_by('id', $user_id)->user_email,
'subscription' => get_user_subscription($user_id),
'download_history' => get_user_downloads($user_id),
);
// Log data export request
log_security_event(array(
'type' => 'data_export',
'user_id' => $user_id,
'requested_by' => get_current_user_id(),
'time' => current_time('mysql'),
));
return $data;
}
CDN Security
Secure CDN-hosted podcast files:
function get_cdn_signed_url($file_path) {
$cdn_key = defined('CDN_SIGNING_KEY') ? CDN_SIGNING_KEY : '';
$expires = time() + 3600;
$policy = base64_encode(json_encode(array(
'url' => $file_path,
'expires' => $expires,
)));
$signature = hash_hmac('sha256', $policy, $cdn_key);
return CDN_BASE_URL . $file_path . '?policy=' . $policy . '&sig=' . $signature;
}
Conclusion
Podcast site security focuses on protecting media files from hotlinking, securing RSS feeds, managing premium content access, and ensuring accurate download analytics. Regular audits help maintain content protection.
Written by Sarah Chen
WP Folder Shield Team