技术头条 - 一个快速在微博传播文章的方式     搜索本站
您现在的位置首页 --> JavaScript --> 对HTML做白名单过滤

对HTML做白名单过滤

浏览:2647次  出处信息

让用户输入HTML的内容是很常见的需求,但是这有一定危险性,可能会带来XSS等问题,因此一般大家都要对HTML进行一定过滤。这个过滤并不容易,如<script />元素自不必说,其他还有如onload或onclick事件,甚至一个普通的<a />元素,它的href中也可以执行JavaScript代码。以前我一直有一段用于过滤的C#实现,一直没有出篓子,似乎也挺靠谱,但最近不知怎么的却发现了问题,可能是C & P出错,也可能原本就有问题,我没有太去关心。但问题总需要解决,于是我想,不如换个角度,基于白名单进行过滤吧。

以前HTML过滤的方式往往是基于黑名单的,例如去除<script />元素及onload事件等等,万一有所遗漏,便会造成安全上的隐患。而白名单策略则正好相反,我们只列出合法的HTML元素及其属性,如果有所遗漏,则最多导致HTML上无法使用某些元素,但不会有安全问题。具体采用哪种方式,就要看您自己的决策了。在我看来,我并不在意进行白名单过滤,因为从我写博客及做项目的经验上来看,用户根本不需要如此广泛的HTML元素以及完整的属性支持,而且这反而会造成样式上的混乱。甚至说,我们只是需要简单的几种元素,如p、a、h1~h6、strong、ul、ol或是li等等,就足够了。样式问题?由上下文环境来统一控制,这才能得到良好的浏览体验

配置

首先,我们要准备一份白名单的配置,其中表明哪些HTML元素,以及其中哪些属性,还有属性的哪些形式是合法的。按照传统,配置往往会采用XML格式。不过我觉得XML虽然还算便于表达,但是冗余信息还是太多,且对于“&”等字符还需要转义,因此有时候并不是配置的理想方式。目前常用的配置格式还有JSON,它不像XML那样冗余,也能较好的表现结构化数据,不过由于我们需要表达正则表达式,使用JSON的话在字符串里的转义就麻烦了。至于其他格式,如ini文件,可能并不容易表示带层级的关系 ,或者需要自己写解析方式,于是我最终还是决定使用XML作为配置形式,如下:

<?xml version="1.0" encoding="utf-8" ?>
<config>
  <tag name="*">
    <attr name="style">^((font|color)\s*:\s*[^;]+;\s*)*$</attr>
  </tag>
  <tag name="a">
    <attr name="href">^[a-zA-Z]+://.+$</attr>
    <attr name="title">.+</attr>
  </tag>
  ...
</config>

根元素下的每个tag元素表示一个合法的HTML元素,其中星号表示对所有元素的统一设置,例如上面的配置便开放了所有元素的style属性中的font和color两个设置,以及a元素中的href和title属性,其中href还必须是“{scheme}://xxx”的形式,这样就避免了“javascript:alert(1)”这样的XSS问题。

有了XML格式,则代码自然也是一蹴而就的:

public class TagConfig : Dictionary<string, Regex>
{
    public TagConfig()
        : base(StringComparer.OrdinalIgnoreCase)
    { }

    public TagConfig(XElement config)
    {
        foreach (var ele in config.Elements("attr"))
        {
            this.Add(ele.Attribute("name").Value, new Regex(ele.Value));
        }
    }
}

public class FilterConfig : Dictionary<string, TagConfig>
{
    public FilterConfig()
        : base(StringComparer.OrdinalIgnoreCase)
    { }

    public FilterConfig(XElement config)
    {
        var wildcardElement = config
            .Elements("tag")
            .SingleOrDefault(e => e.Attribute("name").Value == "*");

        var wildcardConfig = wildcardElement == null ? null :
            new TagConfig(wildcardElement);

        foreach (var ele in config
            .Elements("tag")
            .Where(e => e.Attribute("name").Value != "*"))
        {
            var name = ele.Attribute("name").Value;
            var tagConfig = new TagConfig(ele);

            foreach (var pair in wildcardConfig)
            {
                if (!tagConfig.ContainsKey(pair.Key))
                {
                    tagConfig.Add(pair.Key, pair.Value);
                }
            }

            this.Add(name, tagConfig);
        }
    }
}

在操作时,我将星号中的配置加到每个元素中,这是为了简化操作。如果您愿意,自然也可以独立为星号中的配置再过滤一遍。

过滤策略

这里我不打算对HTML进行合法性验证,例如是否匹配等等,我的目的只是保留合法的HTML元素。因此我的策略很简单,使用几个简单的正则表达式就行了。

首先,我使用下面的正则表达式找出所有的HTML元素:

正则:<[^>]*>
匹配:<a href="http://blog.zhaojie.me/">123321</a>bbc<hello />dde

对于每个HTML元素,我则依次捕获出begin、tag、attr及end四个部分:

正则:^(?<begin></?)(?<tag>[a-zA-z]+)\s*(?<attr>[^>]*?)(?<end>/?>)$
匹配:<a href="http://blog.zhaojie.me">

最后,从上面得到的attr中,再次捕获到属性的键值对:

正则:(?<name>[a-zA-Z]+)\s*=\s*"(?<value>[^"]*)"
匹配:href="http://blog.zhaojie.me/" title="老赵点滴"

最后,再对捕获到的属性及其值进行过滤即可。简单起见,我在这里只考虑由双引号包含的属性值,因为客户端的富文本编辑器可以保证正规方式提交的HTML格式,至于一些Hacker的做法,我只要保证它不会破坏系统就足够了。总体说来,这样的过滤策略并不严谨,但简单粗暴,还算有效好用。

过滤实现

之前描述的策略,使用C#实现只需短短数十行代码:

public class HtmlFilter
{
    private static readonly RegexOptions REGEX_OPTIONS =
        RegexOptions.Compiled | 
        RegexOptions.IgnoreCase | 
        RegexOptions.Singleline;

    // 依次填入上文中三个正则表达式
    private static readonly Regex TAG_REGEX = new Regex(..., REGEX_OPTIONS);
    private static readonly Regex VALID_TAG_REGEX = new Regex(..., REGEX_OPTIONS);
    private static readonly Regex ATTRIBUTE_REGEX = new Regex(..., REGEX_OPTIONS);

    public HtmlFilter() : this(null) { }

    public HtmlFilter(FilterConfig config)
    {
        this.Config = config ?? new FilterConfig();
    }

    public FilterConfig Config { get; private set; }

    public string Filter(string html)
    {
        // 对每个HTML标记进行替换?
        return TAG_REGEX.Replace(html, GetTag);
    }

    private string GetTag(Match match)
    {
        // 如果不是合法的HTML标记形式,则替换为空字符串
        var validTagMatch = VALID_TAG_REGEX.Match(match.Value);
        if (!validTagMatch.Success) return "";

        var tag = validTagMatch.Groups["tag"].Value;

        // 如果这个标记不在白名单中,则替换为空字符串
        TagConfig tagConfig;
        if (!this.Config.TryGetValue(tag, out tagConfig)) return "";

        var begin = validTagMatch.Groups["begin"].Value;
        // 如果是闭合标记,则直接构造并返回
        if (begin == "</")
        {
            return String.Format("</{0}>", tag.ToLower());
        }

        // 过滤出合法的属性键值对
        var attrText = validTagMatch.Groups["attr"].Value;
        var attrMatches = ATTRIBUTE_REGEX.Matches(attrText).Cast<Match>();
        var validAttributes = attrMatches
            .Select(m => GetAttribute(m, tagConfig))
            .Where(s => !String.IsNullOrEmpty(s)).ToArray();

        var end = validTagMatch.Groups["end"].Value;
        // 如果没有合法的属性,则直接构造返回
        if (validAttributes.Length == 0)
        {
            return begin + tag + end;
        }
        else // 否则返回带属性的HTML标记
        { 
            return String.Format(
                "{0}{1} {2}{3}",
                begin,
                tag,
                String.Join(" ", validAttributes),
                end);
        }
    }

    private static string GetAttribute(Match attrMatch, TagConfig tagConfig)
    {
        var name = attrMatch.Groups["name"].Value;

        Regex regex;
        if (!tagConfig.TryGetValue(name, out regex)) return "";
        
        var value = attrMatch.Groups["value"].Value;
        if (regex.IsMatch(value))
        {
            return String.Format("{0}=\"{1}\"", name, value);
        }
        else
        {
            return "";
        }
    }
}

您也可以编写一段与之对应的JavaScript代码,在客户端实现实时过滤(预览)。事实上,我这个博客的评论系统也是用类似的方式实现的。

建议继续学习:

  1. 几种极其隐蔽的XSS注入的防护    (阅读:4626)
  2. 一段Javascript的代码    (阅读:3318)
  3. 过滤部分字段重复的数据    (阅读:2998)
  4. 使用Http-only Cookie来防止XSS攻击    (阅读:3066)
  5. 新浪微博的XSS攻击    (阅读:2946)
  6. PHP Taint – 一个用来检测XSS漏洞的扩展    (阅读:2731)
  7. xss简单渗透测试    (阅读:2634)
  8. 五个实用的Google Analytics过滤设置    (阅读:2200)
  9. 深掘XSS漏洞场景之XSS Rootkit    (阅读:2146)
  10. 过滤字符的性能调优?    (阅读:1999)
QQ技术交流群:445447336,欢迎加入!
扫一扫订阅我的微信号:IT技术博客大学习
© 2009 - 2024 by blogread.cn 微博:@IT技术博客大学习

京ICP备15002552号-1