Skip to content

Security: Zandy/php-template-engine

Security

docs/security.md

安全指南

本文档提供 Zandy_Template 模板引擎的安全使用指南。

目录

变量访问安全

问题说明

⚠️ 重要:默认情况下,模板可以访问所有全局变量,包括系统变量和配置信息。

安全风险

  • 变量泄露:模板可以访问所有全局变量
  • 敏感信息泄露:可能泄露数据库连接信息、API密钥等
  • 变量覆盖:可能导致变量覆盖,影响程序逻辑

解决方案

方案1:白名单模式(推荐用于生产环境)

// 配置白名单,只允许指定的变量被模板访问
$GLOBALS['siteConf']['template_vars_mode'] = 'whitelist';
$GLOBALS['siteConf']['template_vars_whitelist'] = ['user', 'data', 'items'];

// 使用方式不变
$GLOBALS['user'] = $user;
$GLOBALS['data'] = $data;
$html = Zandy_Template::outString('template.htm', $tplDir, $cacheDir);

优点

  • 平衡安全性和易用性
  • 只允许模板访问指定的变量
  • 推荐用于生产环境

方案2:显式传递模式(最安全)

// 显式传递变量,完全控制变量访问
$html = Zandy_Template::outString('template.htm', $tplDir, $cacheDir, false, [
    'user' => $user,
    'data' => $data
]);

优点

  • 最安全,完全控制变量访问
  • 不依赖全局变量
  • 适合高安全要求的场景

安全建议

  1. 开发环境:可以使用完全开放模式(默认),方便调试
  2. 生产环境:推荐使用白名单模式或显式传递模式
  3. 高安全要求:使用显式传递模式,完全控制变量访问
  4. 模板来源不可信:必须使用显式传递模式,并禁用 PHP 代码块

代码执行安全

问题说明

⚠️ 重要:以下语法允许执行任意 PHP 代码,请确保模板来源可信:

  • <!--{php}-->...<!--{/php}--> - 执行任意 PHP 代码
  • <!--{set ...}--> - 设置变量,可执行代码
  • <!--{include ...}--> - 包含 PHP 文件

安全风险

  • 任意代码执行:模板可以执行任意 PHP 代码
  • 系统访问:可以访问文件系统、数据库等
  • 数据泄露:可以读取敏感文件

安全建议

  1. 模板来源可信

    • 如果模板由开发者编写,可以使用这些功能
    • 确保模板文件权限正确
  2. 模板来源不可信

    • 必须禁用 PHP 代码块(如果可能)
    • 使用白名单机制限制可包含的文件
    • 严格验证模板路径
  3. 生产环境

    • 如果模板来源不可信,应禁用这些功能
    • 考虑在模板解析阶段过滤这些语法

路径安全

保护机制

模板引擎会自动验证:

  • ✅ 模板目录必须在 tplBaseDir
  • ✅ 缓存目录必须在 tplCacheBaseDir
  • ✅ 防止路径遍历攻击(使用 realpath() 检查)

路径验证代码

// 模板路径验证
if (!$tplDir2 || !$tplBaseDir || false === stripos($tplDir2, $tplBaseDir)) {
    self::halt('$tplDir is not a valid tpl path', true);
}

// 缓存路径验证
if (!$cacheDir2 || !$tplCacheBaseDir || false === stripos(realpath($cacheDir2), $tplCacheBaseDir)) {
    self::halt('cache path is not valid', true);
}

安全建议

  1. 配置正确的路径

    • 确保 tplBaseDirtplCacheBaseDir 配置正确
    • 使用绝对路径,避免相对路径问题
  2. 文件权限

    • 模板文件:只读权限
    • 缓存目录:可写权限,但限制访问
  3. 路径验证

    • 引擎已自动验证路径,无需额外处理
    • 确保配置的路径正确

HTML 转义和 XSS 防护

转义策略

Zandy_Template 采用手动转义策略:

  • 默认行为:不转义(保持向后兼容)
  • 转义方式:需要转义时手动添加 |escape 修饰符
  • 语法
    • 转义:{$var|escape}htmlspecialchars($var, ENT_QUOTES, 'UTF-8')
    • 不转义:{$var}$var

使用场景

场景 1:用户输入(必须转义)

<!-- 用户输入必须转义,防止 XSS 攻击 -->
用户名:{$username|escape}
评论内容:{$comment|escape}
搜索关键词:{$keyword|escape}

为什么必须转义

  • 用户输入可能包含恶意脚本(如 <script>alert('XSS')</script>
  • 不转义会导致 XSS 攻击
  • 转义后:&lt;script&gt;alert(&#039;XSS&#039;)&lt;/script&gt;(安全)

场景 2:可信内容(可不转义)

<!-- 系统生成的内容,来源可信,可不转义 -->
标题:{$title}
描述:{$description}

什么时候可以不转义

  • 内容由系统生成,不来自用户输入
  • 内容已经过验证和清理
  • 内容来源完全可信

场景 3:HTML 内容(需要特殊处理)

<!-- 如果变量包含 HTML 内容,需要根据情况处理 -->
<!-- 情况1:HTML 来自用户输入,必须转义 -->
用户提交的 HTML:{$userHtml|escape}

<!-- 情况2:HTML 来自系统生成,且需要渲染 -->
系统生成的 HTML:{$systemHtml}
<!-- 注意:确保 $systemHtml 的内容是安全的 -->

最佳实践

  • 如果 HTML 来自用户输入,必须转义(显示为纯文本)
  • 如果 HTML 来自系统生成,且需要渲染,可以不转义(但需确保内容安全)
  • 如果允许用户输入 HTML,应使用 HTML 清理库(如 HTMLPurifier)处理后再输出

转义实现

转义功能通过 filters 插件提供:

// 加载 filters 插件
require_once 'plugins/filters.php';

// 或通过 builtin 插件加载
require_once 'plugins/builtin.php';

转义实现方式

// 内部实现
htmlspecialchars($var, ENT_QUOTES, 'UTF-8')

安全建议

  1. 用户输入必须转义

    <!-- ✅ 正确:转义用户输入 -->
    {$userInput|escape}
    
    <!-- ❌ 错误:不转义用户输入 -->
    {$userInput}
  2. 系统内容可不转义

    <!-- ✅ 可以:系统生成的内容 -->
    {$systemTitle}
    {$systemDescription}
  3. HTML 内容谨慎处理

    <!-- ✅ 推荐:用户 HTML 转义为纯文本 -->
    {$userHtml|escape}
    
    <!-- ⚠️ 谨慎:系统 HTML 不转义(需确保安全) -->
    {$systemHtml}
  4. 使用过滤器链

    <!-- 可以组合多个过滤器 -->
    {$username|upper|escape}  <!-- 先转大写,再转义 -->
    {$description|truncate:50|escape}  <!-- 先截断,再转义 -->

常见错误

❌ 错误示例 1:忘记转义用户输入

<!-- 危险:用户输入未转义 -->
用户名:{$username}
评论:{$comment}

风险:如果用户输入 <script>alert('XSS')</script>,会导致 XSS 攻击。

✅ 正确示例 1:转义用户输入

<!-- 安全:用户输入已转义 -->
用户名:{$username|escape}
评论:{$comment|escape}

❌ 错误示例 2:过度转义系统内容

<!-- 不必要:系统内容不需要转义 -->
标题:{$systemTitle|escape}

说明:虽然不会造成安全问题,但会显示 HTML 实体(如 &lt;),影响显示效果。

✅ 正确示例 2:系统内容不转义

<!-- 正确:系统内容不转义 -->
标题:{$systemTitle}

安全检查清单

  • 所有用户输入是否都使用了 |escape 转义?
  • 表单提交的数据是否都转义了?
  • URL 参数、Cookie 数据是否都转义了?
  • 数据库存储的用户数据输出时是否转义了?
  • HTML 内容是否根据来源正确处理了(用户输入转义,系统生成可不转义)?

最佳实践

开发环境

// 可以使用完全开放模式(默认),方便调试
$GLOBALS['user'] = $user;
$html = Zandy_Template::outString('template.htm', $tplDir, $cacheDir);

生产环境

// 推荐:白名单模式
$GLOBALS['siteConf']['template_vars_mode'] = 'whitelist';
$GLOBALS['siteConf']['template_vars_whitelist'] = ['user', 'data', 'items'];
$GLOBALS['user'] = $user;
$html = Zandy_Template::outString('template.htm', $tplDir, $cacheDir);

高安全要求

// 推荐:显式传递模式
$html = Zandy_Template::outString('template.htm', $tplDir, $cacheDir, false, [
    'user' => $user,
    'data' => $data
]);

模板来源不可信

如果模板来源不可信,必须:

  1. 使用显式传递模式

    $html = Zandy_Template::outString('template.htm', $tplDir, $cacheDir, false, [
        'user' => $user
    ]);
  2. 禁用 PHP 代码块(如果可能):

    • 在模板解析阶段过滤 <!--{php}--> 语法
    • 或使用模板验证机制
  3. 严格验证模板路径

    • 确保模板文件在允许的目录内
    • 使用白名单机制限制可包含的文件

安全检查清单

  • 生产环境是否使用白名单模式或显式传递模式?
  • 模板文件权限是否正确(只读)?
  • 缓存目录权限是否正确(可写但限制访问)?
  • 模板路径配置是否正确?
  • 如果模板来源不可信,是否禁用了 PHP 代码块?
  • 是否使用了 includeTemplate()getTemplateVars() 而不是直接 extract($GLOBALS)

更多信息

There aren’t any published security advisories