java根据白名单过滤html,对HTML做白名单过滤

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

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

配置

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

^((font|color)\s*:\s*[^;]+;\s*)*$

^[a-zA-Z]+://.+$

.+

...

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

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

public classTagConfig: Dictionary

{

publicTagConfig()

: base(StringComparer.OrdinalIgnoreCase)

{ }

publicTagConfig(XElementconfig)

{

foreach(varele inconfig.Elements("attr"))

{

this.Add(ele.Attribute("name").Value, newRegex(ele.Value));

}

}

}

public classFilterConfig: Dictionary

{

publicFilterConfig()

: base(StringComparer.OrdinalIgnoreCase)

{ }

publicFilterConfig(XElementconfig)

{

varwildcardElement = config

.Elements("tag")

.SingleOrDefault(e => e.Attribute("name").Value == "*");

varwildcardConfig = wildcardElement == null? null:

newTagConfig(wildcardElement);

foreach(varele inconfig

.Elements("tag")

.Where(e => e.Attribute("name").Value != "*"))

{

varname = ele.Attribute("name").Value;

vartagConfig = newTagConfig(ele);

foreach(varpair inwildcardConfig)

{

if(!tagConfig.ContainsKey(pair.Key))

{

tagConfig.Add(pair.Key, pair.Value);

}

}

this.Add(name, tagConfig);

}

}

}

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

过滤策略

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

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

正则:]*>

匹配:123321bbcdde

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

正则:^(??)(?[a-zA-z]+)\s*(?[^>]*?)(?/?>)$

匹配:

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

正则:(?[a-zA-Z]+)\s*=\s*"(?[^"]*)"

匹配:href="http://blog.zhaojie.me/" title="老赵点滴"

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

过滤实现

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

public classHtmlFilter{

private static readonlyRegexOptionsREGEX_OPTIONS =

RegexOptions.Compiled |

RegexOptions.IgnoreCase |

RegexOptions.Singleline;

// 依次填入上文中三个正则表达式private static readonlyRegexTAG_REGEX = newRegex(..., REGEX_OPTIONS);

private static readonlyRegexVALID_TAG_REGEX = newRegex(..., REGEX_OPTIONS);

private static readonlyRegexATTRIBUTE_REGEX = newRegex(..., REGEX_OPTIONS);

publicHtmlFilter() : this(null) { }

publicHtmlFilter(FilterConfigconfig)

{

this.Config = config ?? newFilterConfig();

}

publicFilterConfigConfig { get; private set; }

public stringFilter(stringhtml)

{

// 对每个HTML标记进行替换?returnTAG_REGEX.Replace(html, GetTag);

}

private stringGetTag(Matchmatch)

{

// 如果不是合法的HTML标记形式,则替换为空字符串varvalidTagMatch = VALID_TAG_REGEX.Match(match.Value);

if(!validTagMatch.Success) return"";

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

// 如果这个标记不在白名单中,则替换为空字符串TagConfigtagConfig;

if(!this.Config.TryGetValue(tag, outtagConfig)) return"";

varbegin = validTagMatch.Groups["begin"].Value;

// 如果是闭合标记,则直接构造并返回if(begin == "")

{

returnString.Format("{0}>", tag.ToLower());

}

// 过滤出合法的属性键值对varattrText = validTagMatch.Groups["attr"].Value;

varattrMatches = ATTRIBUTE_REGEX.Matches(attrText).Cast();

varvalidAttributes = attrMatches

.Select(m => GetAttribute(m, tagConfig))

.Where(s => !String.IsNullOrEmpty(s)).ToArray();

varend = validTagMatch.Groups["end"].Value;

// 如果没有合法的属性,则直接构造返回if(validAttributes.Length == 0)

{

returnbegin + tag + end;

}

else// 否则返回带属性的HTML标记{

returnString.Format(

"{0}{1} {2}{3}",

begin,

tag,

String.Join(" ", validAttributes),

end);

}

}

private static stringGetAttribute(MatchattrMatch, TagConfigtagConfig)

{

varname = attrMatch.Groups["name"].Value;

Regexregex;

if(!tagConfig.TryGetValue(name, outregex)) return"";

varvalue = attrMatch.Groups["value"].Value;

if(regex.IsMatch(value))

{

returnString.Format("{0}=\"{1}\"", name, value);

}

else{

return"";

}

}

}

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值