PHP / 함수 / md5() / 문자열에서 md5 해시값 생성하는 함수
PHP의 md5()는 입력 문자열의 MD5(Message-Digest Algorithm 5) 해시를 계산합니다. 기본 결과는 32자리 16진수 문자열이고, 두 번째 인수에 true를 지정하면 같은 해시를 16바이트 바이너리로 반환합니다. MD5는 복호화할 암호문을 만드는 함수가 아니며 비밀번호 저장이나 충돌 저항성이 필요한 보안 용도에 사용하면 안 됩니다.
md5()의 입력과 반환 형식
md5() 공식 문서의 첫 인수 string은 해시를 계산할 문자열이고, 선택 인수 binary의 기본값은 false입니다. false면 사람이 읽고 저장하기 쉬운 소문자 16진수, true면 128비트에 해당하는 16바이트 원시 결과가 됩니다. 32와 16은 다른 알고리즘이나 강도의 차이가 아니라 같은 값을 표현하는 방식의 차이입니다.
md5(string: "abc", binary: true)처럼 PHP의 이름 있는 인수를 사용하는 경우에는 현재 인수명인 binary를 사용합니다. 오래된 예제의 raw_output은 과거 설명에서 보이는 이름이므로 현재 코드의 이름 있는 인수와 혼동하지 마세요. 원시 바이너리는 출력 가능한 문자만으로 구성되지 않아 화면에 직접 echo하면 읽기 어려운 문자나 제어 바이트가 나타날 수 있습니다.
전체 예제: 16진수와 바이너리 결과 비교
아래를 UTF-8 파일 md5-demo.php로 저장하고 PHP가 설치된 터미널에서 php md5-demo.php로 실행합니다. 예제는 PHP 8의 명령행 실행을 기준으로 하며 데이터베이스나 외부 라이브러리는 필요하지 않습니다. 화면에서 원시 바이트를 해석하려 하지 않고 bin2hex()로 변환해 비교합니다.
<?php
declare(strict_types=1);
$text = 'abc';
$hex = md5($text);
$raw = md5($text, true);
echo 'hex: ', $hex, PHP_EOL;
echo 'hex bytes: ', strlen($hex), PHP_EOL;
echo 'raw bytes: ', strlen($raw), PHP_EOL;
echo 'raw as hex: ', bin2hex($raw), PHP_EOL;
echo 'same digest: ', ($hex === bin2hex($raw) ? 'yes' : 'no'), PHP_EOL;
hex와 raw as hex는 모두 900150983cd24fb0d6963f7d28e17f72가 됩니다. 길이는 각각 32바이트와 16바이트이며 same digest는 yes입니다. 원시 결과를 16진수로 바꾼 것이 기본 반환값과 같으므로 저장 형식이나 API의 기대 형식이 무엇인지 먼저 확인하세요. 바이너리를 일반 UTF-8 텍스트 열에 그대로 넣는 방식은 적합하지 않을 수 있습니다.
같은 문자열처럼 보여도 결과가 다른 이유
md5()는 화면에 보이는 글자 모양이 아니라 입력 바이트를 처리합니다. 대소문자·끝 공백·줄바꿈·문자 인코딩이 다르면 입력도 달라집니다. 아래 코드를 별도 파일 md5-bytes.php로 저장해 php md5-bytes.php로 실행하면 네 값이 서로 다른 것을 확인할 수 있습니다.
<?php
declare(strict_types=1);
$samples = [
'plain' => 'abc',
'newline' => "abc\n",
'space' => 'abc ',
'uppercase' => 'ABC',
];
foreach ($samples as $label => $text) {
echo $label, ': ', md5($text), PHP_EOL;
}
plain은 앞 예제의 900150983cd24fb0d6963f7d28e17f72이고, newline은 0bee89b07a248e27c83fc3d5951213c1입니다. 텍스트 편집기가 자동으로 붙인 마지막 줄바꿈이나 Windows의 CRLF와 LF 차이도 비교 대상 바이트에 영향을 줄 수 있습니다. 값을 맞추려고 무조건 trim()을 넣으면 원래 의미 있는 공백까지 제거할 수 있으므로 두 시스템이 같은 인코딩·정규화·줄바꿈 규칙을 쓰는지부터 확인합니다.
파일의 내용이 대상이면 md5_file()을 사용합니다. md5("sample.txt")는 파일을 읽는 것이 아니라 sample.txt라는 파일명 문자열 자체를 해시합니다. md5_file()은 읽기 실패 시 false를 반환할 수 있으므로 결과를 문자열처럼 비교하기 전에 실패를 처리해야 합니다.
문자열 해시를 비교할 때 ==를 피해야 하는 이유
PHP의 느슨한 비교와 엄격한 비교 규칙에 따라, 숫자로 해석할 수 있는 두 문자열을 ==로 비교하면 숫자 비교가 이루어질 수 있습니다. 예를 들어 0e 뒤에 숫자가 이어진 문자열은 과학 표기 숫자처럼 해석됩니다. 다음 예제는 두 MD5 문자열이 실제로 같다는 뜻과 느슨한 비교 결과가 다를 수 있음을 보여 줍니다.
<?php
declare(strict_types=1);
$first = md5('240610708');
$second = md5('QNKCDZO');
echo 'same text: ', ($first === $second ? 'yes' : 'no'), PHP_EOL;
echo 'loose comparison: ', ($first == $second ? 'yes' : 'no'), PHP_EOL;
예상 출력은 same text: no와 loose comparison: yes입니다. 두 결과는 서로 다른 16진수 문자열인데도 == 비교가 같은 숫자로 취급한 것입니다. 이것은 서로 다른 입력이 같은 해시 바이트를 만드는 암호학적 충돌과도 구분해야 합니다. 일반 문자열 결과를 정확히 비교하려면 ===를 사용하세요. 비밀값 기반 인증 태그처럼 시간 차이가 문제인 비교에는 hash_equals()를 검토하고, 알려진 값을 첫 인수·사용자 제공 값을 두 번째 인수에 둡니다. 비교 함수를 바꿔도 MD5 자체의 보안 한계가 없어지는 것은 아닙니다.
비밀번호와 보안 검증에 사용할 수 있는가
비밀번호에는 md5() 대신 PHP 비밀번호 해싱 안내에 따른 password_hash()와 password_verify()를 사용합니다. 문제는 같은 입력에 같은 결과를 만든다는 성질만이 아니라, MD5가 매우 빨라 공격자가 많은 비밀번호 후보를 시험하기 쉽다는 점입니다. 단순히 솔트를 붙여 MD5를 한 번 계산하거나 여러 번 감싸는 방식을 새 비밀번호 저장 설계로 사용하지 않습니다.
또한 MD5 보안 고려사항 RFC 6151은 충돌 저항성이 필요한 용도에서 MD5를 사용하지 않도록 설명합니다. MD5 결과가 같다는 사실만으로 악의적인 변경까지 방지했다고 판단할 수 없습니다. 과거 시스템이 정한 MD5 체크섬 형식과의 호환 확인 등 제한된 목적과 보안 인증 목적을 구분하세요.
새 데이터의 일반적인 해시가 필요하면 hash()에서 SHA-256 같은 적합한 알고리즘을 선택할 수 있습니다. 다음은 입력 abc의 SHA-256을 출력하는 독립 예제입니다.
<?php
declare(strict_types=1);
echo hash('sha256', 'abc'), PHP_EOL;
출력은 ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad입니다. 다만 SHA-256 문자열만 함께 제공한다고 출처의 진위까지 보장되지는 않습니다. 공격자가 파일과 체크섬을 둘 다 바꿀 수 있는 상황에서는 신뢰할 수 있는 별도 경로의 값이나 서명 등 해당 검증 체계가 필요합니다. SHA-256도 일반 해시이므로 비밀번호 저장용 password_hash()의 대체재로 생각하지 않습니다.
“MD5 복호화”라고 부르는 조회 서비스는 흔히 미리 계산한 후보나 추측을 통해 입력을 찾습니다. 해시에 일대일 역함수가 있는 것은 아니지만, 짧거나 흔한 입력이 추측되지 않는다는 보장도 없습니다. 비밀번호·인증 토큰·개인정보나 그 해시를 이런 외부 서비스에 입력해 확인하지 마세요.




