检测字符串输入是否包含HTML的正确方法是什么?


What is the correct way to detect whether string inputs contain HTML or not?

当收到表单上的用户输入时,我想检测像"username"或"address"这样的字段是否包含在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和CSFR攻击都需要<>周围的值来让浏览器执行代码(至少从Internet Explorer 6或更高版本)-这是正确的吗?

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

更新

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

更新2:示例

  • 用户提交输入
  • if (mb_strpos($data, '<') === FALSE AND mb_strpos($data, '>') === FALSE)
  • 我保存它

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

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

<h2><?php print $input; ?></h2>'<xml><item><?php print $input; ?></item></xml>

第二种形式更危险,但它仍然应该是安全的:

<input value="<?php print htmlspecialchars($input, ENT_QUOTES, 'UTF-8');?>">

更新3:工作代码

您可以下载我创建的要点,并将代码作为文本或HTML响应运行,以了解我所说的内容。这个简单的检查通过了http://ha.ckers.org XSS Cheat Sheet,但是我找不到任何能通过的东西。(我忽略Internet  ie6及以下版本)。

我开始了另一个赏金,奖励那些能够指出这种方法的问题或其实现中的弱点的人。

更新4:询问DOM

这是我们想要保护的DOM——所以为什么不直接问它呢?帖木儿的回答是这样的:

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
$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";
}
?>

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

阻止浏览器将字符串作为标记处理。

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

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

考虑以下数据:

<strong>Test</strong>

现在,你可以用两种方式来看待这个问题。你可以把它看作文字数据——一个字符序列。您可以将其视为HTML标记,其中包含强烈强调的文本。

如果您只是将其转储到HTML文档中,则将其视为HTML。在那个上下文中,你不能把它当作文字数据。您需要的是输出文字数据的HTML。你需要将编码为HTML。

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

PHP为此提供了几个不同的选项。最直接的是使用htmlspecialchars()将其转换为HTML,然后使用nl2br()将换行符转换为<br>元素。

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

关于官方来源,我可以参考XML规范:

  • 3.1节中的内容生产:在这里,内容由元素、CDATA节、处理指令、注释(必须以<开头)、引用(必须以&开头)和字符数据(包含任何其他合法字符)组成。(虽然前面的>在元素内容中被视为字符数据,但许多人通常将其与<一起转义,因此将其作为特殊字符处理要安全得多。)

  • 2.3节中的属性值生成:一个有效的属性值由引用(必须以&开头)或字符数据(包含任何其他合法字符,但不包括<或用于包装属性值的引号符号)组成。如果您需要在属性中放置字符串输入,除了元素内容之外,还需要检查&<>(以及其他在XML中非法的字符)之外的字符"'

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

HTML5(最新的工作草案,这是一个正在进行的工作,描述了一个非常详细的解析HTML文档算法:

  • 元素内容对应解析算法中的"数据状态"。在这里,字符串输入不应该包含空字符、<(它开始一个新标记)或&(开始字符引用)。
  • 属性值对应"前属性值状态"在解析算法。为简单起见,我们假设属性值用双引号括起来。在这种情况下,解析器移动到"属性值(双引号)状态"。在这种情况下,字符串输入不应该包含空字符,"(结束属性值)或&(开始字符引用)。

如果字符串输入要放置在属性值中(除非放置它们只是为了显示目的),还有一些额外的注意事项要记住。例如,HTML 4指定:

用户代理应该按照如下方式解释属性值:

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

用户代理可以忽略CDATA中的前后空白属性值(。)

属性值规范化也在XML中指定


编辑(2019年4月25日):另外,要怀疑包含—

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

…假设htmlspecialchars没有逃避这些代码点。

HTML净化器做得很好,很容易实现。你也可以使用Zend Framework过滤器,如Zend_Filter_StripTags。

HTML净化器不只是修复HTML。

我想你已经回答了你自己的问题。函数htmlspecialchars()完全可以满足您的需求,但是在将用户输入写入页面之前不应该使用它。要将其存储在数据库中,还有其他函数,如mysqli_real_escape_string()

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

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

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

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

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

你是正确的!所有XSS和CSFR攻击都需要<或>的值,以使浏览器执行代码(至少从IE6+)。

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

这样的格式显示:

<h2><?php print $input; ?></h2> <xml><item><?php print $input; ?></item></xml>

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

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

如果属性是输入值,我们必须引用它,并使用htmlspecialchars这样的函数来编辑相同的内容。

<input value="<?php print htmlspecialchars($input, ENT_QUOTES, 'UTF-8');?>">

同样,即使是<>字符在这里也是安全的。

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

[我们也有很多方法可以安全地将它存储在数据库中,防止SQL漏洞。]

如果用户希望他的"用户名"是&amp; is not an &怎么办?不包含<>…我们会发现并拒绝它吗?我们会接受吗?我们将如何显示它?(这个输入在新的赏金中给出了有趣的结果!)

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

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

我当然不是安全专家,但从我收集的东西来看,就像您建议的

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

应该可以防止你传递被污染的字符串,只要你在那里有你的编码。

不需要'<'或'>'的XSS攻击依赖于在JavaScript块中处理的字符串,然后,从我如何阅读你的问题来看,这不是你在这种情况下所关心的。

我建议您看一下CodeIgniter的xss_clean函数。我知道你不想清洗、消毒或过滤任何东西。你只是想"发现不良行为"并拒绝它。这就是为什么我建议你看一下这个函数代码。

在我看来,我们可以在这里找到深刻而强大的跨站攻击知识,包括你想要和需要的所有知识。

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

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

现在,您当然不需要仅仅因为需要这个函数就使用整个CodeIgniter框架。但我相信你可能想抓住整个CI_Security类(在/system/core/Security.php),并做一些修改,以消除其他依赖。

正如你将看到的,xss_clean代码是相当复杂的,因为XSS漏洞真的是,我只是相信它,不要试图"重新发明这个轮子"…恕我直言,仅通过检测一打字符是无法消除XSS漏洞的。

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

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

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

[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);
?>

如果问题的原因是为了防止XSS,那么有几种方法可以爆发XSS漏洞。关于这一点的一个很好的备忘表是在ha.ckers.org上的XSS备忘表。

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

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

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

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

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

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