【问题标题】:What is the correct way to detect whether string inputs contain HTML or not?检测字符串输入是否包含 HTML 的正确方法是什么?
【发布时间】:2019-04-25 16:38:48
【问题描述】:

当在表单上接收用户输入时,我想检测“用户名”或“地址”等字段是否不包含在 XML(RSS 提要)或 (X)HTML(显示时)中具有特殊含义的标记。

那么,在 HTML 和 XML 上下文中,检测输入的输入是否不包含任何特殊字符的正确方法是?

if (mb_strpos($data, '<') === FALSE AND mb_strpos($data, '>') === FALSE)

if (htmlspecialchars($data, ENT_NOQUOTES, 'UTF-8') === $data)

if (preg_match("/[^\p{L}\-.']/u", $text)) // problem: also caches symbols

我是否遗漏了其他任何内容,例如字节序列或其他棘手的方法来获取诸如“javascript:”之类的标记标签?据我所知,所有XSS and CSFR attacks 都需要&lt;&gt; 围绕值才能让浏览器执行代码(至少来自Internet Explorer 6 或更高版本) - 这是正确的吗?

我不是在寻找减少或过滤输入的东西。我只是想在 XML 或 HTML 上下文中找到危险的字符序列。 (strip_tags() 非常不安全。正如手册所说,它不会检查格式错误的 HTML。)

更新

我想我需要澄清一下,有很多人误认为这个问题是关于通过“转义”或“过滤”危险字符的基本安全问题。这不是那个问题,而且给出的大多数简单答案无论如何都不能解决这个问题。

更新 2:示例

  • 用户提交输入
  • if (mb_strpos($data, '&lt;') === FALSE AND mb_strpos($data, '&gt;') === FALSE)
  • 我保存了

现在数据在我的应用程序中,我用它做两件事 - 1) 以 HTML 之类的格式显示 - 或 2) 在格式元素中显示以进行编辑。

第一个在 XML 和 HTML 上下文中是安全的

&lt;h2&gt;&lt;?php print $input; ?&gt;&lt;/h2&gt;' &lt;xml&gt;&lt;item&gt;&lt;?php print $input; ?&gt;&lt;/item&gt;&lt;/xml&gt;

第二种形式比较危险,但应该还是安全的:

&lt;input value="&lt;?php print htmlspecialchars($input, ENT_QUOTES, 'UTF-8');?&gt;"&gt;

更新 3:工作代码

您可以下载 the gist I created 并将代码作为文本或 HTML 响应运行,以了解我在说什么。这个简单的检查通过了http://ha.ckers.org XSS Cheat Sheet,但我找不到任何可以做到的东西。 (我忽略了 Internet Explorer 6 及以下版本)。

我开始了另一项赏金活动,奖励那些在这种方法上存在问题或在实施过程中存在弱点的人。

更新 4:询问 DOM

这是我们想要保护的 DOM - 那么为什么不直接问它呢? Timur's answer 导致:

function not_markup($string)
{
    libxml_use_internal_errors(true);
    if ($xml = simplexml_load_string("<root>$string</root>"))
    {
        return $xml->children()->count() === 0;
    }
}

if (not_markup($_POST['title'])) ...

【问题讨论】:

  • 你也可以看看php的strip_tags()函数
  • strip_tags() 非常不安全。正如手册所说,它不检查 malformed HTML.
  • 谢谢@webarto,但就像我说的,我不是要修复 HTML。只需阻止浏览器将字符串视为标记即可。
  • @baudday,我不想清理代码。我只是想找出在 HTML 或 XML 上下文中使用时可能有问题的值。我不想过滤或删除任何内容。

标签: php html input xss sanitization


【解决方案1】:

我认为您不需要实现一个庞大的算法来检查字符串是否包含不安全的数据 - 过滤器和正则表达式可以完成工作。但是,如果您需要更复杂的检查,也许这会满足您的需求:

<?php
$strings = array();
$strings[] = <<<EOD
    ';alert(String.fromCharCode(88,83,83))//\';alert(String.fromCharCode(88,83,83))//";alert(String.fromCharCode(88,83,83))//\";alert(String.fromCharCode(88,83,83))//--></SCRIPT>">'><SCRIPT>alert(String.fromCharCode(88,83,83))</SCRIPT>
EOD;
$strings[] = <<<EOD
    '';!--"<XSS>=&{()}
EOD;
$strings[] = <<<EOD
    <SCRIPT SRC=http://ha.ckers.org/xss.js></SCRIPT>
EOD;
$strings[] = <<<EOD
    This is a safe text
EOD;
$strings[] = <<<EOD
    <IMG SRC="javascript:alert('XSS');">
EOD;
$strings[] = <<<EOD
    <IMG SRC=javascript:alert('XSS')>
EOD;
$strings[] = <<<EOD
    <IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>
EOD;
$strings[] = <<<EOD
    perl -e 'print "<IMG SRC=java\0script:alert(\"XSS\")>";' > out
EOD;
$strings[] = <<<EOD
    <SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRIPT>
EOD;
$strings[] = <<<EOD
    </TITLE><SCRIPT>alert("XSS");</SCRIPT>
EOD;



libxml_use_internal_errors(true);
$sourceXML = '<root><element>value</element></root>';
$sourceXMLDocument = simplexml_load_string($sourceXML);
$sourceCount = $sourceXMLDocument->children()->count();

foreach( $strings as $string ){
    $unsafe = false;
    $XML = '<root><element>'.$string.'</element></root>';
    $XMLDocument = simplexml_load_string($XML);
    if( $XMLDocument===false ){
        $unsafe = true;
    }else{

        $count = $XMLDocument->children()->count();
        if( $count!=$sourceCount ){
            $unsafe = true;
        }
    }

    echo ($unsafe?'Unsafe':'Safe').': <pre>'.htmlspecialchars($string,ENT_QUOTES,'utf-8').'</pre><br />'."\n";
}
?>

【讨论】:

  • +1 很有趣,所以您在询问 DOM 引擎该值是否包含子项。有道理,如果它 is HTML 那么它将大于一个孩子。但是,这也假设 PHP DOM 解析器以与浏览器的 DOM 解析器相同的方式查看 HTML。
  • 帖木儿,我想你是我的新英雄。让我再做一些测试。与其试图代表 DOM 引擎猜测 - 让我们问问吧! 太棒了!
  • 是的。而且,如果解析 html 导致错误,youll know it. But dont 忘记了文本(例如我的示例中的“值”)也是一个节点。所以,对于&lt;root&gt;&lt;element&gt;value&lt;/element&gt;&lt;/root&gt; $xml-&gt;children()-&gt;count() 将是1
  • 当然,您的示例中的$xml&lt;root&gt;&lt;/root&gt; 元素。如果您只解析"&lt;root&gt;$string&lt;/root&gt;",那么正确的检查将是$xml-&gt;count()==1,因为SimpleXMLElement::count() 返回子元素的数量。
  • @Xeoncross:看来我的新答案没有引起你的注意...... &amp;amp; is not an &amp; 作为输入字符串呢?您在更新 3 中的代码和在更新 4 中受帖木儿回答启发的新代码都无法处理它。您是否尝试过使用更新 3 代码的 &amp;amp; is not an &amp; 并看到结果?您是否尝试过使用更新 4 代码?请尝试,并请在我的新答案中添加评论,回答我在以“如果用户希望他的“用户名”为......”开头的段落中提出的问题,我真的很好奇,因为你问的问题似乎是“这种方法的问题”......
【解决方案2】:

在上面的评论中,您写道:

只是阻止浏览器将字符串视为标记。

这与标题中的问题完全不同。标题中的方法通常是错误的。剥离标签只会破坏输入,并可能导致数据丢失。曾经尝试在去除标签的博客上谈论 HTML?令人沮丧。

通常正确的解决方案是按照您在评论中所说的那样做 - 阻止浏览器将字符串视为标记。这 - 从字面上理解 - 是不可能的。相反,您所做的是将内容编码为 HTML。

考虑以下数据:

<strong>Test</strong>

现在,您可以从以下两种方式中选择一种。您可以将其视为文字数据 - 一个字符序列。您可以将其视为 HTML - 包含强烈强调文本的标记。

如果您只是将其转储到 HTML 文档中,您将其视为 HTML。在这种情况下,您不能将其视为文字数据。您需要的是可以输出文字数据的 HTML。您需要将其编码为 HTML。

您的问题不在于您的 HTML 太多,而是您的 HTML 太少。当您输出 &amp;lt; 时,您将在 HTML 上下文中输出原始数据。您需要将其转换为 &amp;lt;,这是该数据在输出之前的 HTML 表示形式。

PHP 提供了几个不同的选项来执行此操作。最直接的就是用htmlspecialchars()转成HTML,再用nl2br()把换行符转成&lt;br&gt;元素。

【讨论】:

  • 我想澄清一下,我有非常有效的 HTML 过滤器,它可以在较大的文本(如评论表单或已解析的 HTML 文档)中正确编码 HTML。但是,我只想检查是否存在在小字符串中使用的构成威胁的字符,例如在表单输入中使用的字符。它们不应该被编码,它们应该被拒绝。我还想在解析文档时使用此识别代码来识别它们是否包含 HTML 或 XML 控制字符。
  • 我想补充一点,htmlspecialchars() 非常适合引用由 HTML/XML 标记包装的值 - 但在引用注入 HTML/XML 标记属性的值时会失败。 &lt;tag attr="&lt;?php print htmlspecialchars($t, ENT_QUOTES, 'UTF-8'); ?&gt;"&gt; 不安全,而 &lt;b&gt;&lt;?php print htmlspecialchars($t, ENT_QUOTES, 'UTF-8'); ?&gt;&lt;/b&gt; 安全。
  • @Xeoncross - 你能解释一下为什么它不安全吗?如果你写&lt;div title="&lt;?php print htmlspecialchars('&lt;a href="index.php"&gt;&lt;/a&gt;'); ?&gt;"&gt;Test&lt;/div&gt;,它会输出&lt;div title="&amp;lt;a href=&amp;quot;index.php&amp;quot;&amp;gt;&amp;lt;/a&amp;gt;"&gt;Test&lt;/div&gt;,这是正确的转义,因此不危险。
  • htmlspecialchars 仅处理 &amp;"'&lt;&gt;。当你有 javascript:alert(); 这样的东西时,你会完全避免使用那个过滤器。
  • @Xeoncross,这仅在用于onmouseoverhref 之类的情况下才有效。而且您可能不会直接将用户输入的内容(即使已转义)输入到这些属性中,我希望?或者更一般地说,我认为安全性很大程度上取决于上下文。
【解决方案3】:

如果您只是“为print '&lt;h3&gt;' . $name . '&lt;/h3&gt;' 寻找保护”,那么是的,至少 第二种方法就足够了,因为它会检查该值是否会被解释为标记,如果不是 逃脱了。 (本例中$name出现的区域为元素内容,只有字符&amp;&lt;&gt;出现在元素内容中时具有特殊含义。)(对于href和类似的属性,可能需要检查“JavaScript:”,但正如您在评论中所说,这不是目标。)

官方来源可以参考XML specification

  • Content production in section 3.1:这里的内容由元素、CDATA 部分、处理指令和 cmets(必须以 &lt; 开头)、引用(必须以 &amp; 开头)和字符数据 (其中包含任何其他法律字符)。 (虽然前导&gt;在元素内容中被视为字符数据,但很多人通常将其与&lt;一起转义,将其视为特殊更安全。)

  • Attribute value production in section 2.3:有效的属性值由引用(必须以 &amp; 开头)或字符数据(包含任何其他合法字符,但不包含 &lt; 或用于换行的引号符号组成属性值)。如果您需要将字符串输入放置在属性中除了元素内容之外,还需要检查字符"' 以及&amp;&lt;,可能还有@987654347 @(以及其他在 XML 中非法的字符)。

  • Section 2.2:定义哪些 Unicode 代码点在 XML 中是合法的。特别是,null 在 XML 文档中是非法的,并且可能无法在 HTML 中正确显示。

HTML5(latest working draft,这是一项正在进行的工作,描述了一个非常精细的解析 HTML 文档的算法:

  • 元素内容对应解析算法中的"data state"。 这里,字符串输入不应包含空字符、&lt;(它开始一个新标签)或&amp; (开始一个字符引用)。
  • 属性值对应"before attribute value state" 在解析算法中。 为简单起见,我们假设属性值用双引号括起来。在这种情况下,解析器移动到 "attribute value (double-quoted) state"。 在这种情况下,字符串输入不应包含空字符、"(结束属性值)或&amp;(开始字符引用)。

如果要将字符串输入放置在属性值中(除非将它们放置在此处仅用于显示目的),请牢记其他注意事项。例如,HTML 4 specifies:

用户代理应该如下解释属性值:

  • 用字符替换字符实体,
  • 忽略换行,
  • 用一个空格替换每个回车符或制表符。

用户代理可能会忽略 CDATA 中的前导和尾随空格 属性值[.]

属性值规范化也在XML中指定 规范,但显然不在 HTML5 中。


编辑(2019 年 4 月 25 日):另外,请注意包含——

  • 空代码点(因为它可能在某些地方导致解析错误,如 HTML5 规范中所指定),或
  • XML 中的任何代码点都是非法的(因为它会在读取 XML 文档时导致解析错误),

...假设 htmlspecialchars 没有转义这些代码点。

【讨论】:

  • 太棒了,有人理解我的问题。然而,这只是我已经说过的话的重申,所以我正在寻找比你和我现有知识更官方的来源。
  • 感谢 Peter,现在也许有人可以为独立用户代理(浏览器和手机)以及 HTML 4 和 5 添加源代码。
【解决方案4】:

HTML Purifier 做得很好并且很容易实现。你也可以使用像 Zend_Filter_StripTags 这样的Zend Framework 过滤器。

HTML Purifier 不只是修复HTML

【讨论】:

  • 这可能会解决识别在某些情况下构成威胁的字符的问题,但仅识别 2-12 个可能的字符也是 极大的内存浪费我正在寻找的序列。 HTMLPurifier 是为过滤而不是定位危险输入而构建的。
【解决方案5】:

我想你回答了你自己的问题。 htmlspecialchars() 函数完全符合您的需要,但在将用户输入写入页面之前不应使用它。要将其存储在数据库中,还有其他功能,例如mysqli_real_escape_string()

根据经验,对于给定的目标系统,您应该只在需要时转义用户输入:

  1. 转义用户输入通常意味着丢失原始数据,不同的目标系统(HTML输出/SQL/执行)需要不同的转义。它们甚至可能相互冲突。
  2. 无论如何,您都必须出于给定目的转义数据,总是。您甚至不应该信任数据库中的条目。所以从用户输入读取时转义并没有什么大的优势,但是双重转义会导致无效数据。

与转义相比,验证内容是尽早做的一件好事。如果您期望一个整数,则只接受整数,否则拒绝用户输入。

【讨论】:

  • mysql-_real_escape-string 不会删除 html 标签,这就是问题所在
  • @user705339 - 它没有也不应该,这就是我试图用这个答案说的。仅在需要时为特定目的逃生。 HTML标签存储在数据库中是可以的,这并不危险,但是当在HTML页面上显示它时,你应该使用'htmlspecialchars()'来使这个标签无害。即使您认为数据库中没有存储任何标签,您也应该这样做。至于 javascript,通常 javascript 需要 HTML 标签才能运行,必须将此用户输入写入事件属性以便执行。
【解决方案6】:

检测字符串输入是否包含HTML标签的正确方法, 或任何其他在显示时(不是实体)在 XML 或 (X)HTML 中具有特殊含义的标记只是

if (mb_strpos($data, '&lt;') === FALSE AND mb_strpos($data, '&gt;') === FALSE)

你是对的!所有 XSS 和 CSFR 攻击都需要围绕值的 以使浏览器执行代码(至少从 IE6+ 开始)。

考虑到给定的输出上下文,这足以安全地以 HTML 等格式显示:

&lt;h2&gt;&lt;?php print $input; ?&gt;&lt;/h2&gt; &lt;xml&gt;&lt;item&gt;&lt;?php print $input; ?&gt;&lt;/item&gt;&lt;/xml&gt;

当然,如果我们在输入中有任何实体,例如&amp;aacute;,浏览器不会将其输出为&amp;aacute;,而是输出为á,除非我们在输出时使用htmlspecialchars之类的函数.在这种情况下,即使&lt;&gt; 也是安全的。

在使用字符串输入作为属性值的情况下,安全性取决于属性。

如果属性是输入值,我们必须引用它并使用htmlspecialchars 之类的函数,以便返回相同的内容进行编辑。

&lt;input value="&lt;?php print htmlspecialchars($input, ENT_QUOTES, 'UTF-8');?&gt;"&gt;

同样,即使 &lt;&gt; 字符在这里也是安全的。

我们可以得出结论 如果我们总是使用htmlspecialchars 来输出它,我们不必对输入进行任何类型的检测和拒绝,并且我们的上下文将始终适合上述情况(或同样安全的)。

[而且我们还有很多方法可以将其安全地存储在数据库中,防止 SQL 漏洞利用。]

如果用户希望他的“用户名”是&amp;amp; is not an &amp; 怎么办?它不包含&lt; 也不包含&gt;...我们会检测并拒绝它吗?我们会接受吗?我们将如何展示它? (这个输入在新的赏金中给出了有趣的结果!)

最后,如果我们的上下文扩展,并且我们将使用字符串输入作为 anchor href,那么我们的整个方法会突然发生巨大变化。但是这个场景不包含在问题中。

(值得一提的是,即使使用htmlspecialchars,如果每个步骤的字符编码不同,字符串输入的输出也可能会有所不同。)

【讨论】:

  • 嘿!新的赏金已经改变!我想我赢了:&amp;amp; is not an &amp; :-)(基于 René Magritte 的“这不是管道”)
  • 你说得对,我错过了这个答案。无论如何,这是一个问题。但是,这不是安全问题 - 更多的是显示问题。就像您看到可能被双重转义的字符串Hi I&amp;amp;quote;m John 一样。 &amp;amp; 用于字符显示,不会像' " &lt; &gt; 一样干扰解析。 Sill,正如您提到的,当用于 &lt;input value=" " "/&gt; 之类的东西时,同样的事情也适用于引号。
【解决方案7】:

我当然不是安全专家,但从我收集到的内容类似于您的建议

if (htmlspecialchars($data, ENT_NOQUOTES, 'UTF-8') === $data)

应该可以防止你传递受污染的字符串,因为你的编码就在那里。

XSS 不需要 '' 的攻击依赖于在 JavaScript 块中处理的字符串,从我如何阅读您的问题来看,这不是您的身份在这种情况下担心。

【讨论】:

    【解决方案8】:

    我建议你看看CodeIgniter 中的xss_clean 函数。我知道你不想清洁、消毒或过滤任何东西。您只想“检测不良行为”并拒绝它。这正是我建议您查看此功能代码的原因。

    IMO,我们可以在那里找到深入而强大的XSS 漏洞知识,包括您想要和需要的所有知识。

    那么,我对您的简短/直接回答是:

    if (xss_clean($data) === $data)
    

    现在,您当然不需要仅仅因为需要这个单一功能就使用整个 CodeIgniter 框架。但我相信您可能想要获取整个 CI_Security 类(/system/core/Security.php)并进行一些修改以消除其他依赖项。

    正如您将看到的,xss_clean 代码非常复杂,因为 XSS 漏洞确实如此,我只是相信它,不要试图“重新发明这个轮子”......恕我直言,你无法摆脱XSS 漏洞仅检测到十几个字符。

    【讨论】:

    • 感谢@JBruni,几年前我向 CI 团队报告了有关此功能的一些安全漏洞。我想我应该再看看那个函数,看看它是如何演变的。
    • 好吧,他们要解决的问题是再次获取文本并将其变为safe for immediate display,而无需进一步处理。在我上面提到的示例中,这完全填充了 A 和 B。问题在于,它还删除了在正确上下文的情况下应该在文本中允许的许多内容。如果您尝试讨论 XSS 中的任何内容,它将被删除,从而导致问题。我认为上下文过滤应该在上下文之前完成 - 而不是一般意义上的。
    • @Xeoncross 我承认,当我写下我的答案时,我仍然没有完全理解你想要什么(ed)......这并不容易......我的意思是:你想要什么的想法,本身,很容易,但是......你知道,让这个简单的想法被其他人理解并不容易(如果是这样,我们就不需要写这么多来达成协议)......就像试图解释一盏灯是什么以及它是如何工作的,在它存在和工作之前...... [待续]
    • 最初的问题(检测字符串输入是否包含 HTML 的正确方法是什么?)很棘手……实际上,我们可以说 Hello, World 是纯 HTML。 2 &amp;lt; 5 ... 是否包含 HTML 呢?如果您的意思是 HTML 标记,那么,看来您的第一个代码选项是正确的答案。
    • 确实,CI xss_clean 有很多严重的缺陷,但它们似乎已在最新版本 (2.1.0) 中得到纠正。而且,事实上,整个讨论,包括问题,开始变得有意义,因为 context 被逐渐解释......即,你想要做什么,作为 输出,与您的输入...
    【解决方案9】:

    filter_input + FILTER_SANITIZE_STRING(有很多flag可供选择)

    :- http://www.php.net/manual/en/filter.filters.sanitize.php

    【讨论】:

    • 有趣,虽然STRIP_LOWSTRIP_HIGH 让我觉得这只对ASCII 有用。我需要进一步研究...
    【解决方案10】:

    如果您知道允许的字符集,则可以使用正则表达式。如果用户名中有一个不允许的字符,则抛出错误:

    [a-zA-Z0-9_.-]
    

    在这里测试你的正则表达式:http://www.perlfect.com/articles/regextutor.shtml

    <?php
    $username = "abcdef";
    $pattern = '/[a-zA-Z0-9_.-]/';
    preg_match($pattern, $username, $matches);
    print_r($matches);
    ?>
    

    【讨论】:

    • 这不支持 unicode 字符。我认为你需要多学习一些正则表达式,这样你才能支持非英语单词。如果我想要单词字符,if(preg_match_all('/\P{L}/u', $text)) 会很好用。
    • 反转正则表达式并查找 HTML <.>
    • 这是个坏主意。他们不需要注入完整的标签 - 在大多数情况下只是标签的一部分。
    • 只搜索开头 然后。
    【解决方案11】:

    如果问题的原因是为了防止XSS,有几种方法可以爆破XSS 漏洞。一个很棒的备忘单是XSS Cheatsheet at ha.ckers.org

    但是,在这种情况下检测是没有用的。您只需要预防,在将文本输入保存到数据库之前正确使用 htmlspecialchars/htmlentities 比检测错误输入更快更好。

    【讨论】:

    • HTML 编码是输出问题,而不是输入问题。在将数据保存到数据库之前执行此操作是错误的。这是为了输出编码,应该在你从数据库中提取数据之后完成。
    • @Jim 你不认为这违反了 DRY 原则吗?既然可以清理一次输入,为什么还要在每次渲染页面时清理输出?
    • @Greg 因为您并不总是知道数据将如何输出。即使您只输出到网页,您也可能需要将数据插入到查询字符串、JavaScript 或 HTML 中,其中每个都有不同的控制字符和转义协议。
    【解决方案12】:

    Regex 仍然是解决问题的最有效方法。无论您计划使用或建议使用什么框架,最有效的方法仍然是自定义正则表达式代码。您可以使用正则表达式测试字符串,并使用 htmlcharacter 函数删除(或转换)受影响的部分。
    无需安装任何其他框架,或使用一些冗长的应用程序。

    【讨论】:

      【解决方案13】:

      您可以使用PHP 中的strip_tags 函数。此函数将从给定数据中去除 HTML 和 PHP 标记。

      例如,$data 是保存您的内容的变量,那么您可以这样使用:

      if (strlen($data) != strlen(strip_tags($data))){
          return false;
      } 
      else{
          return true;
      }
      

      它将根据原始内容检查剥离的内容。如果两者都相等,那么我们可以希望没有任何HTML标签,它返回true。否则,它会返回 false,因为它找到了一些 HTML 标记。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-12-27
        • 1970-01-01
        • 2014-12-20
        • 2022-08-07
        • 1970-01-01
        • 1970-01-01
        • 2020-07-12
        相关资源
        最近更新 更多