PHP / 현재 페이지의 URL 정보 가져오는 방법
PHP에서는 $_SERVER['REQUEST_URI']로 요청 경로와 쿼리 문자열을, $_SERVER['QUERY_STRING']으로 물음표 뒤의 쿼리 부분을 확인합니다. 실행 중인 스크립트 경로는 SCRIPT_NAME 등과 구분해야 하며, 전체 공개 URL을 만들 때는 요청의 Host 헤더를 그대로 신뢰하지 않고 서비스에서 정한 공개 주소를 사용하는 것이 안전합니다.
요청 URL과 실행 파일 경로 구분
다음 주소로 PHP 파일을 직접 요청한다고 가정합니다. URL 재작성이나 프록시가 없는 일반적인 구성의 설명 예시이며 실제 값은 웹 서버·PHP 실행 방식에 따라 달라질 수 있습니다.
https://www.example.com:8443/tools/url-info.php?q=a%20b&n=2#result
| 값 | 예시 | 의미 |
|---|---|---|
| HTTP_HOST | www.example.com:8443 | 요청 Host 헤더. 포트가 포함될 수 있음 |
| REQUEST_URI | /tools/url-info.php?q=a%20b&n=2 | 일반적인 직접 요청의 경로와 쿼리 |
| QUERY_STRING | q=a%20b&n=2 | ?를 제외한 쿼리 문자열 |
| SCRIPT_NAME | /tools/url-info.php | 실행 중인 주 스크립트의 URL 경로 |
| PHP_SELF | /tools/url-info.php | 환경에 따라 PATH_INFO가 포함될 수 있는 현재 스크립트 경로 |
| basename(SCRIPT_NAME) | url-info.php | 스크립트 경로 마지막 부분 |
PHP의 $_SERVER 문서는 모든 환경이 같은 항목을 제공한다고 보장하지 않습니다. CLI에서는 HTTP 요청 관련 값이 없거나 의미가 다를 수 있으므로 ??로 기본값을 두고 웹 요청으로 확인합니다. SCRIPT_FILENAME·__FILE__·DOCUMENT_ROOT는 서버 파일 시스템 경로이므로 브라우저 URL과 섞지 않습니다. 내부 서버 경로를 공개 진단 화면에 불필요하게 노출하지 마세요.
REQUEST_URI에는 보통 퍼센트 인코딩이 남아 있습니다. $_GET['q']는 이미 해석된 값이므로 예시에서는 a b가 됩니다. urldecode() 안내처럼 $_GET에 디코딩을 다시 적용하면 값의 의미가 달라질 수 있습니다. URL 전체를 한 번에 디코딩해 경로의 %2F나 쿼리의 &를 구분자로 바꾸는 일도 피합니다.
실제 요청 값을 안전하게 출력하는 전체 예제
url-info.php를 UTF-8로 저장합니다. 아래 코드는 필요한 항목만 확인하며 모든 출력 값을 HTML 텍스트 문맥에 맞게 이스케이프합니다. 쿼리의 q가 배열로 전달될 수 있으므로 문자열인지도 검사합니다.
<?php
declare(strict_types=1);
header('Content-Type: text/html; charset=UTF-8');
header('Cache-Control: no-store');
function serverText(string $key): string
{
$value = $_SERVER[$key] ?? '';
return is_string($value) ? $value : '';
}
function escapeUrlInfo(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$requestUri = serverText('REQUEST_URI');
$scriptName = serverText('SCRIPT_NAME');
$q = $_GET['q'] ?? '';
$values = [
'HTTP_HOST' => serverText('HTTP_HOST'),
'REQUEST_URI' => $requestUri,
'REQUEST_PATH' => explode('?', $requestUri, 2)[0],
'QUERY_STRING' => serverText('QUERY_STRING'),
'SCRIPT_NAME' => $scriptName,
'PHP_SELF' => serverText('PHP_SELF'),
'SCRIPT_BASENAME' => basename($scriptName),
'GET q' => is_string($q) ? $q : '(문자열이 아닙니다)',
];
?>
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>요청 URL 정보</title>
</head>
<body>
<h1>요청 URL 정보</h1>
<dl>
<?php foreach ($values as $key => $value): ?>
<dt><?= escapeUrlInfo($key) ?></dt>
<dd><?= escapeUrlInfo($value) ?></dd>
<?php endforeach; ?>
</dl>
</body>
</html>
파일이 있는 폴더에서 아래 명령으로 로컬 서버를 실행하고 http://127.0.0.1:8080/url-info.php?q=a%20b&n=2를 엽니다. 개발 서버는 실습용이며 종료할 때 Ctrl+C를 누릅니다. HTTP_HOST는 127.0.0.1:8080, REQUEST_PATH는 /url-info.php, QUERY_STRING은 q=a%20b&n=2로 보이고 GET q에는 공백이 해석된 a b가 나옵니다.
php -S 127.0.0.1:8080
화면의 값이 같아 보이는 직접 요청에서도 PHP_SELF를 “항상 쿼리만 뺀 요청 URL”로 정의하면 안 됩니다. /url-info.php/extra처럼 PATH_INFO를 받거나 모든 요청을 index.php로 처리하는 라우팅에서는 실행 스크립트와 방문자가 요청한 경로가 달라질 수 있습니다. htmlspecialchars()는 HTML 출력용이며 URL 검증이나 인가 검사를 대신하지 않습니다.
전체 공개 URL은 신뢰할 수 있는 주소로 구성
HTTP_HOST는 클라이언트가 보낸 헤더에서 비롯될 수 있습니다. 이를 무검증으로 붙여 비밀번호 재설정 링크·canonical·리디렉션 주소를 만들면 다른 호스트가 들어갈 수 있습니다. OWASP의 Host 헤더 검증 안내도 이런 위험을 설명합니다. SERVER_NAME 역시 서버 설정에 따라 요청의 영향을 받을 수 있으므로 만능 대체 값으로 보지 않습니다.
단일 도메인 서비스에서 공개 주소를 표시해야 한다면 서버 설정으로 확정한 오리진(origin: 스킴·호스트·포트)을 사용합니다. 다음 public-url.php는 https://www.example.com이 설정된 공개 주소라고 가정하며 실제 서비스의 값으로 바꿉니다. 요청 경로를 붙이는 예제이고 임의 URL의 유효성·권한을 검증하는 범용 함수는 아닙니다.
<?php
declare(strict_types=1);
const APP_ORIGIN = 'https://www.example.com';
header('Content-Type: text/plain; charset=UTF-8');
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
if (!is_string($requestUri) || !str_starts_with($requestUri, '/')
|| str_starts_with($requestUri, '//') || str_contains($requestUri, '\\')
|| preg_match('~[\x00-\x20\x7F#]~', $requestUri)) {
http_response_code(400);
exit('지원하지 않는 요청 경로입니다.');
}
echo APP_ORIGIN . $requestUri;
로컬에서 /public-url.php?q=a%20b로 요청하면 출력은 https://www.example.com/public-url.php?q=a%20b입니다. example.com으로 실제 접속하는 코드는 아니며 설정 주소와 현재 요청의 경로·쿼리를 합쳐 표시합니다. Host 헤더가 다른 값이어도 APP_ORIGIN은 바뀌지 않습니다. HTML 링크에 넣을 때는 속성 문맥에 맞는 이스케이프를 추가하고, canonical에서는 추적 매개변수 등을 포함할지 별도 정책을 정합니다. 비밀번호 재설정 같은 링크는 검증한 고정 경로와 필요한 매개변수만으로 생성합니다.
HTTPS·프록시·#조각이 다르게 보이는 이유
직접 TLS를 종료하는 서버에서는 HTTPS 환경 값으로 접속 방식을 확인할 수 있습니다. 다음 scheme.php는 일반적인 on·off 관례를 처리하는 단순 확인 예제입니다. 실제 웹 서버가 어떤 값을 제공하는지 확인한 뒤 사용하며 모든 프록시 구성을 자동 판별하는 코드는 아닙니다.
<?php
$https = $_SERVER['HTTPS'] ?? '';
$isHttps = is_string($https) && $https !== '' && strtolower($https) !== 'off';
header('Content-Type: text/plain; charset=UTF-8');
echo $isHttps ? 'https' : 'http';
앞단 프록시가 HTTPS를 종료하고 PHP 서버에는 HTTP로 전달하면 외부 접속은 HTTPS여도 위 코드는 http를 표시할 수 있습니다. X-Forwarded-Proto·X-Forwarded-Host·Forwarded 값을 누구나 보낼 수 있는 상태에서 그대로 신뢰하지 말고, 신뢰하는 프록시의 주소와 헤더 덮어쓰기 정책을 서버·프레임워크에서 구성합니다. 단일 도메인의 공개 링크에는 앞 절처럼 확정한 오리진을 쓰면 내부 전송 방식과 분리할 수 있습니다.
주소의 #result는 프래그먼트(fragment, 문서 안의 위치를 가리키는 조각)이며 일반 HTTP 요청에서 서버로 전송되지 않습니다. 따라서 PHP의 REQUEST_URI를 조합해도 주소 표시줄의 # 뒷부분까지 복원할 수는 없습니다. 브라우저에서 그 값이 꼭 필요하면 JavaScript로 읽고 필요한 범위만 명시적으로 서버에 전달하는 별도 흐름을 설계합니다. URL 쿼리에는 토큰·개인정보가 있을 수 있으므로 진단 출력과 로그를 공개 환경에 남길 때도 범위를 제한하세요.




