文件签名(魔术字节)一览
文件开头若干字节中包含的签名(魔术字节)一览,可用于判断文件类型与实现校验。
什么是魔术字节
多数文件格式在开头都有固定的字节序列(签名),称为魔术字节或文件签名。操作系统和应用程序据此判断文件类型。扩展名可以轻易伪造,而魔术字节属于二进制层面的标识,因此校验可靠性更高。
Linux 的 file 命令和 PHP 的 finfo 类都用这种方式判断 MIME 类型。
图像文件
| 格式 | 扩展名 | 魔术字节(十六进制) | ASCII 表示 | 偏移量 |
|---|---|---|---|---|
| PNG | .png | 89 50 4E 47 0D 0A 1A 0A | ‰PNG\r\n\x1a\n | 0 |
| JPEG | .jpg/.jpeg | FF D8 FF | — | 0 |
| GIF87a | .gif | 47 49 46 38 37 61 | GIF87a | 0 |
| GIF89a | .gif | 47 49 46 38 39 61 | GIF89a | 0 |
| WebP | .webp | 52 49 46 46 ?? ?? ?? ?? 57 45 42 50 | RIFF????WEBP | 0 |
| BMP | .bmp | 42 4D | BM | 0 |
| TIFF(小端序) | .tiff/.tif | 49 49 2A 00 | II*\0 | 0 |
| TIFF(大端序) | .tiff/.tif | 4D 4D 00 2A | MM\0* | 0 |
| AVIF / HEIF | .avif/.heif | 66 74 79 70 | ftyp | 4 |
| ICO | .ico | 00 00 01 00 | — | 0 |
| SVG | .svg | <?xml 或 <svg | 文本 | 0(无 BOM) |
文档与压缩包
| 格式 | 扩展名 | 魔术字节(十六进制) | 备注 |
|---|---|---|---|
25 50 44 46 2D | %PDF- | ||
| ZIP | .zip | 50 4B 03 04 | PK\x03\x04 |
| ZIP(空文件) | .zip | 50 4B 05 06 | — |
| GZIP | .gz | 1F 8B | — |
| 7-Zip | .7z | 37 7A BC AF 27 1C | 7z\xBC\xAF'\x1C |
| RAR4 | .rar | 52 61 72 21 1A 07 00 | Rar!\x1A\x07\x00 |
| RAR5 | .rar | 52 61 72 21 1A 07 01 00 | — |
| TAR | .tar | 75 73 74 61 72 | ustar(偏移 257) |
| DOCX / XLSX / PPTX | .docx 等 | 50 4B 03 04 | 基于 ZIP(靠扩展名区分) |
| XLS(旧格式) | .xls | D0 CF 11 E0 A1 B1 1A E1 | OLE2 容器 |
| DOC(旧格式) | .doc | D0 CF 11 E0 A1 B1 1A E1 | OLE2 容器 |
文本与数据格式
| 格式 | 扩展名 | 魔术字节 / BOM | 备注 |
|---|---|---|---|
| 带 BOM 的 UTF-8 | .txt / .csv 等 | EF BB BF | BOM(Byte Order Mark) |
| UTF-16 LE BOM | .txt 等 | FF FE | 小端序 |
| UTF-16 BE BOM | .txt 等 | FE FF | 大端序 |
| UTF-32 LE BOM | .txt 等 | FF FE 00 00 | — |
| JSON(常规) | .json | 7B 或 5B | { 或 [ |
| CSV(常规) | .csv | 文本(无 BOM) | 无签名 |
| XML | .xml | 3C 3F 78 6D 6C | <?xml |
音频与视频
| 格式 | 扩展名 | 魔术字节(十六进制) | 备注 |
|---|---|---|---|
| MP3 | .mp3 | FF FB / FF F3 / FF F2 | 或 ID3 标签:49 44 33 |
| MP4 / M4A / M4V | .mp4 等 | 66 74 79 70 | ftyp(偏移 4) |
| WAV | .wav | 52 49 46 46 ?? ?? ?? ?? 57 41 56 45 | RIFF????WAVE |
| OGG | .ogg/.ogv | 4F 67 67 53 | OggS |
| FLAC | .flac | 66 4C 61 43 | fLaC |
| AVI | .avi | 52 49 46 46 ?? ?? ?? ?? 41 56 49 20 | RIFF????AVI |
| MKV / WebM | .mkv/.webm | 1A 45 DF A3 | EBML 头 |
可执行文件与其他
| 格式 | 扩展名 | 魔术字节(十六进制) | 备注 |
|---|---|---|---|
| ELF(Linux 可执行文件) | 无 / .elf | 7F 45 4C 46 | \x7FELF |
| PE(Windows 可执行文件) | .exe/.dll | 4D 5A | MZ |
| Mach-O(macOS 可执行文件) | 无 | CF FA ED FE | 64bit |
| PHP 脚本 | .php | 3C 3F 70 68 70 | <?php |
| SQLite | .sqlite/.db | 53 51 4C 69 74 65 20 66 6F 72 6D 61 74 20 33 00 | SQLite format 3\0 |
| ISO | .iso | 43 44 30 30 31 | 偏移 32769:CD001 |
PHP 中的魔术字节校验示例
['offset' => 0, 'bytes' => "\x89PNG\r\n\x1a\n", 'length' => 8],
'jpg' => ['offset' => 0, 'bytes' => "\xFF\xD8\xFF", 'length' => 3],
'gif' => ['offset' => 0, 'bytes' => "GIF8", 'length' => 4],
'pdf' => ['offset' => 0, 'bytes' => "%PDF-", 'length' => 5],
'zip' => ['offset' => 0, 'bytes' => "PK\x03\x04", 'length' => 4],
'exe' => ['offset' => 0, 'bytes' => "MZ", 'length' => 2],
];
if (!isset($signatures[$expectedType])) {
throw new \InvalidArgumentException("未知的文件类型: $expectedType");
}
$sig = $signatures[$expectedType];
$handle = fopen($filePath, 'rb');
if ($sig['offset'] > 0) fseek($handle, $sig['offset']);
$header = fread($handle, $sig['length']);
fclose($handle);
if (substr($header, 0, $sig['length']) !== $sig['bytes']) {
throw new \RuntimeException("魔数字节不匹配(期望: $expectedType)");
}
}
// 使用示例
validateMagicBytes($_FILES['upload']['tmp_name'], 'png');
echo "这是合法的 PNG 文件";
相关资源
- MIME 类型一览 — 扩展名与 Content-Type 对照表
- 损坏文件 / 头部被篡改的文件 — 用于校验测试
- 文件校验实现清单
❓ 常见问题
为什么只检查扩展名不够?
扩展名只是文件名的一部分,攻击者可以随意更改:把 shell.php 改名为 shell.jpg,扩展名检查就通过了。魔数查看的是数据的真实开头,改名骗不过它。但它对伪造前几个字节的多态文件仍然薄弱,因此请把扩展名、MIME 类型与魔数三者交叉验证。
识别 PNG 与 JPEG 需要读取多少字节?
PNG 需要 8 字节(89 50 4E 47 0D 0A 1A 0A),JPEG 只需前 3 字节(FF D8 FF)。实现上读取约 12 字节即可一次覆盖主要格式,ZIP(50 4B 03 04)与 PDF(25 50 44 46)也在这一范围内。无需把整个文件读入内存。
Office 文档和 APK 被判定为 ZIP。
这个判定是正确的。docx、xlsx、pptx、APK、JAR、EPUB 都是 ZIP 容器,开头均为 50 4B 03 04。要区分它们必须解开压缩包查看内部:docx 看 [Content_Types].xml,APK 看 AndroidManifest.xml。仅凭魔数无法做到放行 docx 而拦截 APK。