Safe Redirect Builder
Builds an internal redirect URL only if the target matches approved local paths.
Function signature
ogSafeRedirectUrl(candidate_url, allowed_prefixes = array(), fallback_url = '/')
Categories
- Security
Parameters
candidate_urlCandidate URL from a form or return parameter.allowed_prefixesOptional approved local path prefixes.fallback_urlLocal fallback path.Return value
Short public-safe status message.
- url
Compatibility
Existing function name and call order preserved; metadata signature corrected to source.
Minimum PHP version: 7.4
Security notes
Validate request method, identity, permissions, and caller-owned allowlists before use; keep secrets out of public output.
Code
<?php
/*
* Copyright (c) 2026 Jeffery L. Paris <jparis@phpog.com>.
* Free for personal and internal use. Paid project use requires visible credit
* to Jeffery L. Paris. Corporate use requires a paid license fee unless a
* separate written license states otherwise.
*/
/**
* Builds a safe internal redirect URL from a candidate path.
*
* External URLs, protocol-relative URLs, CR/LF header injection attempts, and
* traversal-shaped paths are rejected. Only local site paths are returned.
*
* @param string $candidate_url Candidate URL from a form or return parameter.
* @param array $allowed_prefixes Optional approved local path prefixes.
* @param string $fallback_url Local fallback path.
* @return array Safe redirect result with selected URL.
*/
function ogSafeRedirectUrl($candidate_url, $allowed_prefixes = array(), $fallback_url = '/') {
$result = array(
'success' => false,
'message' => '',
'data' => array('url' => '/')
);
$candidate_url = trim((string)$candidate_url);
$fallback_url = trim((string)$fallback_url);
if (empty($fallback_url) || substr($fallback_url, 0, 1) != '/') {
$fallback_url = '/';
}
$result['data']['url'] = $fallback_url;
if (empty($candidate_url)) {
$result['success'] = true;
$result['message'] = 'Fallback redirect selected.';
return $result;
}
if (preg_match('/[\r\n]/', $candidate_url)) {
$result['message'] = 'Redirect contains invalid control characters.';
return $result;
}
if (strpos($candidate_url, '//') === 0) {
$result['message'] = 'Protocol-relative redirect rejected.';
return $result;
}
if (preg_match('/^[a-z][a-z0-9+.-]*:/i', $candidate_url)) {
$result['message'] = 'External redirect rejected.';
return $result;
}
if (substr($candidate_url, 0, 1) != '/') {
$result['message'] = 'Redirect must be a local path.';
return $result;
}
if (strpos($candidate_url, '/..') !== false || strpos($candidate_url, '..') === 0) {
$result['message'] = 'Redirect path traversal rejected.';
return $result;
}
$allowed = false;
if (empty($allowed_prefixes)) {
$allowed = true;
} else {
foreach ($allowed_prefixes as $prefix) {
$prefix = trim((string)$prefix);
if (!empty($prefix) && substr($prefix, 0, 1) == '/') {
if (strpos($candidate_url, $prefix) === 0) {
$allowed = true;
}
}
}
}
if (!$allowed) {
$result['message'] = 'Redirect path is not approved.';
return $result;
}
$result['success'] = true;
$result['message'] = 'Safe redirect accepted.';
$result['data']['url'] = $candidate_url;
return $result;
}