在 PHP 後端渲染頁面時,常需將服務端生成的 HTML 片段、用戶輸入內容、模板數據傳遞給前端 JavaScript 變量。若直接拼接字符串,極易導致 XSS 跨站腳本攻擊 或 JavaScript 語法錯誤。本文提供一個生產級的 PHP 轉義函數,深度解析轉義規則、編碼陷阱與現代替代方案。
jsHtmlEscape()<?php
/**
* 將任意字符串安全編碼為 JavaScript 字符串字面量內容
* 適用場景:內聯腳本、JSON 嵌入、HTML data-* 屬性、JS 模板插值
*
* @param string $input 原始輸入
* @param string $encoding 字符編碼(默認 UTF-8)
* @return string 可直接放入 JS 單雙引號字符串中的安全內容
*/
function jsHtmlEscape($input, $encoding = 'UTF-8') {
if (!is_string($input)) {
$input = (string) $input;
}
// 1. 統一換行符為 \n(JS 字符串中需轉義)
$input = str_replace(["\r\n", "\r", "\n"], '\n', $input);
// 2. 核心轉義序列(按順序執行,避免重複轉義)
$map = [
'\\' => '\\\\', // 反斜桿最優先(防止後續轉義被再次轉義)
'"' => '\\"', // 雙引號
"'" => "\\'", // 單引號
'' => '<\/', // 關鍵:防止 過早閉合外層標簽
'&' => '\\x26', // & 符號(十六進制轉義,避免 HTML 實體干擾)
'<' => '\\x3C', // <
'>' => '\\x3E', // >
// 可選:控制字符轉義(U+0000-U+001F)
];
$result = strtr($input, $map);
// 3. 處理 Unicode 非 BMP 字符(Emoji 等),轉為 \uXXXX\uXXXX 代理對
// 注意:json_encode 會自動處理,但這裡手動實現以避免引入 JSON 結構
$result = preg_replace_callback('/[\x{10000}-\x{10FFFF}]/u', function ($m) {
$code = hexdec(bin2hex(iconv('UTF-8', 'UTF-32BE', $m[0])));
$high = 0xD800 + (($code - 0x10000) >> 10);
$low = 0xDC00 + (($code - 0x10000) & 0x3FF);
return sprintf('\\u%04X\\u%04X', $high, $low);
}, $result);
return $result;
}
// 使用示例
$userBio = '<script>alert("XSS")</script> 它說: "Hello" & '\'World\'';
$safeJs = jsHtmlEscape($userBio);
// 在內聯腳本中安全使用
echo "<script>
var userBio = '{$safeJs}'; // 單引號包裹
console.log(userBio);
</script>";
?>
json_encode() 不夠用?許多開發者習慣用 json_encode($str) 直接輸出 JS 字符串,但存在隱患:
JSON_UNESCAPED_UNICODE 保留中文,但舊版本輸出 `\uXXXX`,可能不符合預期。正確用法: `var x = = json_encode($str, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>;` 但需去除首尾引號或調整模板。
| 原始字符 | JS 字符串轉義 | HTML 實體 | JSON 轉義 | 備註 |
|---|---|---|---|---|
| \ (反斜桿) | \\ | \ | \\ | 最優先處理 |
| ' (單引號) | \' | ' | \' (JSON_HEX_APOS) | 單引號字符串必須 |
| " (雙引號) | \" | " | \" (JSON_HEX_QUOT) | 雙引號字符串必須 |
| < / > | \x3C / \x3E | < / > | \u003C / \u003E (JSON_HEX_TAG) | 防 攻擊 |
| & | \x26 | & | \u0026 (JSON_HEX_AMP) | 避免 HTML 實體干擾 |
| 換行/回車 | \n / \r | / | \n / \r | JS 中不可有裸換行 |
| Emoji (😀) | \uD83D\uDE00 | 😀 | \uD83D\uDE00 | 代理對形式 |
反模式(內聯腳本):
<script>
var config = { apiUrl: '= jsHtmlEscape($apiUrl) ?>', user: '= jsHtmlEscape($username) ?>' };
// 極易出錯、難維護、CSP 策略受限
</script>
推薦模式(HTML data-* + ES Module):
<!-- 頁面模板 -->
<div id="app"
data-api-url="= htmlspecialchars($apiUrl, ENT_QUOTES, 'UTF-8') ?>"
data-user-name="= htmlspecialchars($username, ENT_QUOTES, 'UTF-8') ?>"
></div>
<!-- 入口腳本 -->
<script type="module">
import { initApp } from './app.js';
const el = document.getElementById('app');
initApp({
apiUrl: el.dataset.apiUrl,
userName: el.dataset.userName
});
</script>
優勢:
總結: PHP 向 JavaScript 傳遞數據的核心原則是「語境感知輸出編碼」。理解 HTML、JavaScript、JSON 三種語境的轉義規則差異,優先採用「數據屬性 + 模塊化 JS」架構避開內聯腳本坑洼,必要時使用經過嚴格測試的轉義函數。安全不僅是防 XSS,更是消除語法錯誤、編碼亂碼、維護噩夢的系統性工程。