从HTML内容中删除脚本标记

74

我正在使用HTML Purifier (http://htmlpurifier.org/)。

我只想删除<script>标记,不想删除行内格式或其他任何内容。

我该如何实现这个目的?

还有一件事,是否有其他方法可以从HTML中删除脚本标记?


3
记住,脚本标签不是 HTML 中唯一容易受攻击的部分。 - Karolis
3
请阅读 这篇文章。它将对你有所帮助。 - Jose Adrian
4
@Jose 绝对不行。看这个链接:https://dev59.com/X3I-5IYBdhLWcg3wq6do#1732454,没有正则表达式可以解析HTML。 - Madara's Ghost
1
@Rikudo 嗯...如果他需要使用正则表达式来删除HTML标记...一定有原因。感谢您提供的链接! - Jose Adrian
@Rikudo Sennin -- 或者说 PHP 根本不行。 :) - Michael Lorton
显示剩余8条评论
13个回答

162
因为这个问题被标记了,所以我会提供一个穷人的解决方案:
$html = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $html);

然而,正则表达式并不适用于解析HTML/XML,即使你编写了完美的表达式,它最终也会出现问题,这不值得。尽管在某些情况下,使用正则表达式可以快速修复一些标记,但由于其只是解决临时问题,所以要忽略安全性。仅在您信任的内容/标记上使用正则表达式。

请记住,用户输入的任何内容都应被视为不安全

在这里,一个更好的解决方案是使用专门设计用于此目的的DOMDocument。以下是演示如何使用DOMDocument完成相同任务的代码片段,它易于编写,比起正则表达式来说更加干净(几乎)可靠和(近乎)安全:

<?php

$html = <<<HTML
...
HTML;

$dom = new DOMDocument();

$dom->loadHTML($html);

$script = $dom->getElementsByTagName('script');

$remove = [];
foreach($script as $item)
{
  $remove[] = $item;
}

foreach ($remove as $item)
{
  $item->parentNode->removeChild($item); 
}

$html = $dom->saveHTML();

我有意删除了HTML,因为即使这样也会破坏它。


13
不推荐使用正则表达式解决该问题。请参考这个讨论 - Alex
55
我很久以前看过那个讨论,你应该阅读它而不仅仅是浏览它。 - Dejan Marjanović
12
虽然我欣赏你的冷漠回应,但我反对你的答案是有道理的。请参阅此要点,其中提供了一个精心编制的脚本标记,可以绕过你的正则表达式。公平地说,这可能更多地是你特定正则表达式的缺陷,而不是放弃正则表达式的理由。但对我来说仍然很有趣。 - Alex
5
如果您想采取正则表达式的方式,请确保多次运行“prey_replace”,直到输出不再更改(此处捕获@ParijatKalia的示例输入)。 - Mark
3
由于您无法获得正确的结果(迭代器的行为与预期不同),因此请查看此评论。链接 - Dejan Marjanović
显示剩余17条评论

44

使用 PHP DOMDocument 解析器。

$doc = new DOMDocument();

// load the HTML string we want to strip
$doc->loadHTML($html);

// get all the script tags
$script_tags = $doc->getElementsByTagName('script');

$length = $script_tags->length;

// for each tag, remove it from the DOM
for ($i = 0; $i < $length; $i++) {
  $script_tags->item($i)->parentNode->removeChild($script_tags->item($i));
}

// get the HTML string back
$no_script_html_string = $doc->saveHTML();

我用以下HTML文档,这个方法对我有效:

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <title>
            hey
        </title>
        <script>
            alert("hello");
        </script>
    </head>
    <body>
        hey
    </body>
</html>

请记住,DOMDocument解析器需要PHP 5或更高版本。


5
我已经厌倦了听有关正则表达式和HTML的讨论。在某些非常特殊的情况下,使用正则表达式是可以接受的。在我的情况下,我遇到了这个错误:“Warning: DOMDocument::loadHTML() [domdocument.loadhtml]: Tag myCustomTag invalid in Entity”。我已经尝试了所有方法。我只想删除应用程序的一个小部分中的脚本标记(_而且_不想再花更多时间)。我将使用preg_replace,就这样。我不想再听到其他任何话题了。 :) - Yes Barry
2
请看我对最佳答案的评论。我更希望程序员能够涵盖一般情况,因为恶意用户可能会变得非常聪明。然而,你是正确的:例如在开发内部应用程序时,可以考虑忽略这些漏洞并使用正则表达式。 - Alex
1
DOMDocument和SimpleXML可用于加载文档根目录之外的文件。使用libxml_disable_entity_loader(true)禁用libxml的此功能。http://www.php.net/manual/en/function.libxml-disable-entity-loader.php - txyoji
一旦您有一个空标签,例如<script src="..."></script>,此代码将会返回'Fatal error: Call to a member function removeChild() on null' - yumba
@Spi 很有趣。你知道如何修改代码来解决这个问题吗? - Alex
显示剩余5条评论

7
$html = <<<HTML
...
HTML;
$dom = new DOMDocument();
$dom->loadHTML($html);
$tags_to_remove = array('script','style','iframe','link');
foreach($tags_to_remove as $tag){
    $element = $dom->getElementsByTagName($tag);
    foreach($element  as $item){
        $item->parentNode->removeChild($item);
    }
}
$html = $dom->saveHTML();

我点赞了这个回复,因为首先它干净简洁,其次它也提醒我iframes也可能会给我带来麻烦。 - soger
1
另外,我刚刚意识到,这会添加doctype、html和body标签,对于当前的问题来说是可以的,但对我来说不行,但我只需要更改一行代码(正如保存HTML php.net页面上的顶部注释所说):$dom->loadHTML($html,LIBXML_HTML_NOIMPLIED|LIBXML_HTML_NODEFDTD); - soger

4
通过操纵字符串可以实现简单的方法。
function stripStr($str, $ini, $fin)
{
    while (($pos = mb_stripos($str, $ini)) !== false) {
        $aux = mb_substr($str, $pos + mb_strlen($ini));
        $str = mb_substr($str, 0, $pos);
        
        if (($pos2 = mb_stripos($aux, $fin)) !== false) {
            $str .= mb_substr($aux, $pos2 + mb_strlen($fin));
        }
    }

    return $str;
}

@Someone_who_likes_SE 当然可以。你可以使用 stripos 和 substr 来代替 mb_stripos 和 mb_substr,但是我更喜欢使用 MB 函数,它们更可靠。 - José Carlos PHP
这一切都很好,但是这里有一个严重的缺陷。请注意,您不知道输入是什么。如果$fin不在$str(或$aux)中,则会出现完美的循环。祝你调试愉快!有几个选项可以调整此代码以应对该缺陷。我会让你来修复它。 - kklepper
@kklepper,我已经修改了它,现在如果找不到$fin,它会从$ini处截断到字符串的末尾。问候! - José Carlos PHP

4

尝试使用这个完整灵活的解决方案。它能够完美地运行,并在某种程度上基于之前的答案,但还包含其他验证检查,并且可以去掉loadHTML(...)函数中的其他隐含的HTML。它被分成了两个单独的函数(一个有先前的依赖关系,因此不要重新排序/重新排列),因此您可以将其用于想要同时删除的多个HTML标签(即不仅限于'script'标签)。例如,removeAllInstancesOfTag(...)函数接受一个标签名称的数组,或者可选地只接受一个标签作为字符串。所以,不再拖延,这就是代码:


/* Remove all instances of a particular HTML tag (e.g. <script>...</script>) from a variable containing raw HTML data. [BEGIN] */

/* Usage Example: $scriptless_html = removeAllInstancesOfTag($html, 'script'); */

if (!function_exists('removeAllInstancesOfTag'))
    {
        function removeAllInstancesOfTag($html, $tag_nm)
            {
                if (!empty($html))
                    {
                        $html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'); /* For UTF-8 Compatibility. */
                        $doc = new DOMDocument();
                        $doc->loadHTML($html,LIBXML_HTML_NOIMPLIED|LIBXML_HTML_NODEFDTD|LIBXML_NOWARNING);

                        if (!empty($tag_nm))
                            {
                                if (is_array($tag_nm))
                                    {
                                        $tag_nms = $tag_nm;
                                        unset($tag_nm);

                                        foreach ($tag_nms as $tag_nm)
                                            {
                                                $rmvbl_itms = $doc->getElementsByTagName(strval($tag_nm));
                                                $rmvbl_itms_arr = [];

                                                foreach ($rmvbl_itms as $itm)
                                                    {
                                                        $rmvbl_itms_arr[] = $itm;
                                                    }

                                                foreach ($rmvbl_itms_arr as $itm)
                                                    {
                                                        $itm->parentNode->removeChild($itm);
                                                    }
                                            }
                                    }
                                else if (is_string($tag_nm))
                                    {
                                        $rmvbl_itms = $doc->getElementsByTagName($tag_nm);
                                        $rmvbl_itms_arr = [];

                                        foreach ($rmvbl_itms as $itm)
                                            {
                                                $rmvbl_itms_arr[] = $itm;
                                            }

                                        foreach ($rmvbl_itms_arr as $itm)
                                            {
                                                $itm->parentNode->removeChild($itm); 
                                            }
                                    }
                            }

                        return $doc->saveHTML();
                    }
                else
                    {
                        return '';
                    }
            }
    }

/* Remove all instances of a particular HTML tag (e.g. <script>...</script>) from a variable containing raw HTML data. [END] */

/* Remove all instances of dangerous and pesky <script> tags from a variable containing raw user-input HTML data. [BEGIN] */

/* Prerequisites: 'removeAllInstancesOfTag(...)' */

if (!function_exists('removeAllScriptTags'))
    {
        function removeAllScriptTags($html)
            {
                return removeAllInstancesOfTag($html, 'script');
            }
    }

/* Remove all instances of dangerous and pesky <script> tags from a variable containing raw user-input HTML data. [END] */


以下是一个测试用例示例:


$html = 'This is a JavaScript retention test.<br><br><span id="chk_frst_scrpt">Congratulations! The first \'script\' tag was successfully removed!</span><br><br><span id="chk_secd_scrpt">Congratulations! The second \'script\' tag was successfully removed!</span><script>document.getElementById("chk_frst_scrpt").innerHTML = "Oops! The first \'script\' tag was NOT removed!";</script><script>document.getElementById("chk_secd_scrpt").innerHTML = "Oops! The second \'script\' tag was NOT removed!";</script>';
echo removeAllScriptTags($html);

我希望我的答案真正能帮助到某些人。祝您使用愉快!


3
  • 这是 ClandestineCoderBinh WPO 的合并。

脚本标签箭头的问题在于它们可以有多个变体。

例如(< = &lt; = &amp;lt;)和(> = &gt; = &amp;gt;

因此,不要创建一个具有无数变体的模式数组,我认为更好的解决方案是

return preg_replace('/script.*?\/script/ius', '', $text)
       ? preg_replace('/script.*?\/script/ius', '', $text)
       : $text;

这将删除任何看起来像 script.../script 的内容,无论是什么箭头代码或变体,您可以在此处进行测试:https://regex101.com/r/lK6vS8/1


3

简化版:

$html = preg_replace("/<script.*?\/script>/s", "", $html);

在使用正则表达式时,可能会出现问题,所以最好这样做:

$html = preg_replace("/<script.*?\/script>/s", "", $html) ? : $html;

这样当“意外”发生时,我们会得到原始的 $html 而不是空字符串。


3
function remove_script_tags($html){
    $dom = new DOMDocument();
    $dom->loadHTML($html);
    $script = $dom->getElementsByTagName('script');

    $remove = [];
    foreach($script as $item){
        $remove[] = $item;
    }

    foreach ($remove as $item){
        $item->parentNode->removeChild($item);
    }

    $html = $dom->saveHTML();
    $html = preg_replace('/<!DOCTYPE.*?<html>.*?<body><p>/ims', '', $html);
    $html = str_replace('</p></body></html>', '', $html);
    return $html;
}

Dejan的回答很好,但是saveHTML()会添加不必要的doctype和body标签,这应该被去掉。请参见https://3v4l.org/82FNP


3
不是loadHTML(...)函数会添加那些内容,而是它会使用LIBXML_HTML_NODEFDTDLIBXML_HTML_NOIMPLIED。具体信息可以参考这里:https://www.php.net/manual/en/libxml.constants.php。 - James Anderson Jr.
好的,谢谢詹姆斯澄清! - relipse

2

修改 ctf0 的回答的示例。这个代码只会执行一次 preg_replace,同时检查错误并阻止斜杠字符编码。

$str = '<script> var a - 1; <&#47;script>'; 

$pattern = '/(script.*?(?:\/|&#47;|&#x0002F;)script)/ius';
$replace = preg_replace($pattern, '', $str); 
return ($replace !== null)? $replace : $str;  

如果您正在使用PHP 7,您可以使用null合并运算符来进一步简化它。
$pattern = '/(script.*?(?:\/|&#47;|&#x0002F;)script)/ius'; 
return (preg_replace($pattern, '', $str) ?? $str); 

这确实有一个缺陷,即如果有人在HTML中使用脚本文件夹中的文件,例如:<img src="/script/email/img.jpg">.. <img src="/script/email/img-0.jpg">。这将创建一个陷阱,会删除它们之间的所有内容。 - tech-e

1
这是Dejan Marjanovic答案的简化版本:
function removeTags($html, $tag) {
    $dom = new DOMDocument();
    $dom->loadHTML($html);
    foreach (iterator_to_array($dom->getElementsByTagName($tag)) as $item) {
        $item->parentNode->removeChild($item);
    }
    return $dom->saveHTML();
}

可以用来删除任何类型的标签,包括<script>标签:

$scriptlessHtml = removeTags($html, 'script');

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接