Skip to content
← Back to Functions
Code

URL Allowlist Validator

Checks whether a URL belongs to an approved scheme, host, and path policy.

Function signature

ogValidateAllowedUrl(url, allowed_hosts = array(), allowed_schemes = array('https'), options = array())

Categories

  • Forms and Validation

Parameters

urlCandidate URL to validate.allowed_hostsApproved host names. Empty means any public host is allowed.allowed_schemesApproved schemes. Defaults to https only.optionsOptional path_prefixes and allow_private_ip flags. Recognized keys: `allow_private_ip`, `path_prefixes`.

Return value

Short public-safe status message.

  • valid
  • url
  • scheme
  • host
  • path

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 and internal paths 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.
 */

/**
 * Checks whether a URL belongs to an approved scheme, host, and path policy.
 *
 * Primary use case: Webhook callbacks, redirects, imported links.
 * Typical inputs: candidate URL, allowed host list, scheme list.
 * Typical output: validated URL or empty string.
 *
 * Implementation note: Reject localhost/private IP targets for server-side fetches.
 *
 * @param string $url Candidate URL to validate.
 * @param array $allowed_hosts Approved host names. Empty means any public host is allowed.
 * @param array $allowed_schemes Approved schemes. Defaults to https only.
 * @param array $options Optional path_prefixes and allow_private_ip flags.
 * @return array Structured validation result.
 */
function ogValidateAllowedUrl($url, $allowed_hosts = array(), $allowed_schemes = array('https'), $options = array()) {
	$result = array(
		'success' => false,
		'message' => '',
		'data' => array(
			'url' => '',
			'scheme' => '',
			'host' => '',
			'path' => '',
			'valid' => false
		)
	);

	$url = trim((string)$url);
	if (!is_array($allowed_hosts)) {
		$allowed_hosts = array();
	}
	if (!is_array($allowed_schemes) || empty($allowed_schemes)) {
		$allowed_schemes = array('https');
	}
	if (!is_array($options)) {
		$options = array();
	}

	if (empty($url)) {
		$result['message'] = 'Missing URL.';
		return $result;
	}

	if (preg_match('/[\x00-\x1F\x7F]/', $url)) {
		$result['message'] = 'URL contains control characters.';
		return $result;
	}

	if (strpos($url, '//') === 0) {
		$result['message'] = 'Protocol-relative URLs are not allowed here.';
		return $result;
	}

	$parts = parse_url($url);
	if (empty($parts) || empty($parts['scheme']) || empty($parts['host'])) {
		$result['message'] = 'URL must include a scheme and host.';
		return $result;
	}

	$scheme = strtolower($parts['scheme']);
	$host = strtolower($parts['host']);
	$path = '/';
	if (!empty($parts['path'])) {
		$path = $parts['path'];
	}

	$approved_schemes = array();
	foreach ($allowed_schemes as $allowed_scheme) {
		$allowed_scheme = strtolower(trim((string)$allowed_scheme));
		if (!empty($allowed_scheme)) {
			$approved_schemes[] = $allowed_scheme;
		}
	}

	if (!in_array($scheme, $approved_schemes, true)) {
		$result['message'] = 'URL scheme is not approved.';
		return $result;
	}

	if (!empty($allowed_hosts)) {
		$host_allowed = false;
		foreach ($allowed_hosts as $allowed_host) {
			$allowed_host = strtolower(trim((string)$allowed_host));
			if (!empty($allowed_host) && $host === $allowed_host) {
				$host_allowed = true;
			}
		}
		if (!$host_allowed) {
			$result['message'] = 'URL host is not approved.';
			return $result;
		}
	}

	$allow_private_ip = false;
	if (!empty($options['allow_private_ip'])) {
		$allow_private_ip = true;
	}

	if (!$allow_private_ip && filter_var($host, FILTER_VALIDATE_IP)) {
		$public_ip = filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
		if ($public_ip === false) {
			$result['message'] = 'Private or reserved IP targets are not allowed.';
			return $result;
		}
	}

	if (!$allow_private_ip) {
		if ($host === 'localhost' || substr($host, -10) === '.localhost') {
			$result['message'] = 'Localhost targets are not allowed.';
			return $result;
		}
	}

	if (!empty($options['path_prefixes']) && is_array($options['path_prefixes'])) {
		$path_allowed = false;
		foreach ($options['path_prefixes'] as $prefix) {
			$prefix = (string)$prefix;
			if (!empty($prefix) && strpos($path, $prefix) === 0) {
				$path_allowed = true;
			}
		}
		if (!$path_allowed) {
			$result['message'] = 'URL path is not approved.';
			return $result;
		}
	}

	$result['success'] = true;
	$result['message'] = 'URL validated.';
	$result['data'] = array(
		'url' => $url,
		'scheme' => $scheme,
		'host' => $host,
		'path' => $path,
		'valid' => true
	);

	return $result;
}