velocity 不转义html,改造Velocity模板引擎让$[!]{}输出默认进行html转义,并增加$#{}语法支持不转义输出...

一直以来在项目中使用Apache Velocity模板引擎作为视图层输出,为了解决XSS漏洞,需要对输出到页面的内容进行HTML转义,我一般采用2种方式实现:

使用过滤器 Filter,在其中进行 HttpServletRequestWrapper的 getParameter( )等方法重载,在底层进行HTML转义,然后页面直接输出;

这种方式很轻松很直接,业务代码不需要修改就完成了所有的转义工作;但是也带来了问题:修改了用户的原始输入数据,如果需要用到用户的原始输入数据,又得反转义回去,很麻烦。

在页面上使用 velocity-tools 的 $esc.html( ) 进行手工转义:$esc.html($task.content);

这种方式工作量比较大,需要在转义的变量上增加 $esc.html(),很容易遗漏。

那有没有别的方法来解决呢?

后来我发现 Velocity 为我们提供了EscapeHtmlReference  event handler 用于在引用变量值输出之前进行HTML转义处理,但是这个HTML转义功能需要通过eventhandler.escape.html.match=//配置指定变量前缀来进行,特别对于有的变量需要转义,有的变量不需要转义的情况下非常的不方便;在用了一段时间后,感觉很麻烦,污染变量命名,不爽;

正巧这段时间也使用了artTemplate.js的前端模板引擎,发现这个模板引擎提供了2种变量输出方式: 和 ,其中 是默认的常用输出方式(会对变量值中的HTML字符进行转义输出), 则是原样输出(不进行任何转义);感觉这样的方式非常的好,即满足了大部分的转义输出,也满足了小部分的非转义输出,而且不用对业务代码进行修改,而是由模板引擎提供不同的输出方法。

那能否修改 Velocity 的语法,也支持一种非转义输出呢?这样不就完美解决这个问题了吗?

$[ ! ][ { ] param [ } ] 是 Velocity 的默认引用定义和输出语法,为了自定义的语法简单易用,想采用 $#{ ... } 语法格式,这样和原来的语法只有一个字的差别:! -> #

// 默认转义输出

$task.content

${task.content}

$!task.content

$!{task.content}

// 原样不转义输出

$#task.content

$#{task.content}

于是我开始研究 Velocity 的语法解析代码,Velocity 采用 AST 语法树进行模板的语法解析的,所有的语法定义在 Parser.jjt 文件中,然后使用JAVACC 编译Parser.jjt文件生成语法解析代码(ASTAddNode,ASTEQNode, ASTReference, ASTSetDirective等等),$[ ! ][ { ] ... [ } ]语法Velocity定义为 Reference,因此 ASTReference.java 就是用来处理 Reference的。

在 Parser.jjt 文件的 730 行定义了这样的语法配置:

这段语法是用来支持 $! 的,如果我把最后的"!" 变成 ("!"|"#") 不就支持 $# 了嘛,呵呵,修改之(当然其它的语法定义也必须都看一遍):

发现在 ASTReference.java 文件的 getRoot() 方法中进行了 $!和$!{ 的处理:

if (t.image.startsWith("$!")) {

referenceType = QUIET_REFERENCE;

/*

*  only if we aren‘t escaped do we want to null the output

*/

if (!escaped) nullString = "";

if (t.image.startsWith("$!{")) {

/*

*  ex : $!{provider.Title}

*/

return t.next.image;

} else {

/*

*  ex : $!provider.Title

*/

return t.image.substring(2);

}

}

在有了上面的了解后,修改Velocity的Reference 语法规则就简单了,迅速修改 Parser.jjt 文件,然后使用 JAVACC(我使用javacc-eclipse插件) 编译Parser.jjt,生成了和Velocity src 下源码一样的AST代码结构,由于我修改的仅仅是 Reference 的语法,因此生成的代码文件中我只保留了 ParserTokenManager.java 文件(用于替换src的该文件,这个文件代码很多,又没有发现可以上传附件的地方,因此代码就不贴出了),其它的文件使用原始 src 下的文件(注意:原始src下的AST文件大部分被后期手工修改完善,因此不能完全使用新编译生成的AST代码文件)。

同时对原始的 ASTReference.java 的getRoot() 方法中的代码进行扩充,支持 $# 的处理:

if (t.image.startsWith("$!") || t.image.startsWith("$#")) {

referenceType = QUIET_REFERENCE;

/*

*  only if we aren‘t escaped do we want to null the output

*/

if (!escaped) nullString = "";

if (t.image.startsWith("$!{") || t.image.startsWith("$#{"))

{

/*

*  ex : $!{provider.Title} OR $#{provider.Title}

*/

return t.next.image;

}

else

{

/*

*  ex : $!provider.Title OR $#provider.Title

*/

return t.image.substring(2);

}

}

然后测试,一切OK,这样 Velocity 就多了一个语法:$#[ { ] ... [ } ]支持。

哦,还没完呢,呵呵,此时 $#{} 仅仅和 $!{} 一样,下面开始对这2个命令进行处理:$!{} -- 转义输出, $#{} -- 不转义输出:

为了不修改原始核心代码,方便扩展,这里采用扩展 Velocity 提供的 ReferenceInsertionEventHandler 接口实现进行(类似EscapeHtmlReference),直接上代码和配置:

import org.apache.velocity.app.event.ReferenceInsertionEventHandler;

import org.apache.velocity.runtime.RuntimeServices;

import org.apache.velocity.util.RuntimeServicesAware;

/**

* HTML转义输出

*/

public class VelocityEscapeHtmlOutput

implements ReferenceInsertionEventHandler, RuntimeServicesAware

{

private RuntimeServices rs = null;

public Object referenceInsert(String reference, Object value)

{

// 呵呵,这里... 凡是以 $#开头的reference,其值直接返回(^_^)

if(reference.startsWith("$#")) {

return value;

}

// 其它默认转义

return escapeHtml(value);

}

public void setRuntimeServices(RuntimeServices rs)

{

this.rs = rs;

}

protected RuntimeServices getRuntimeServices()

{

return this.rs;

}

/**

* 转义HTML字符串

* @param str

* @return

*/

private static Object escapeHtml(Object value)

{

if(value == null)

{

return null;

}

if(!(value instanceof String))

{

return value;

}

String str = value.toString();

StringBuilder sb = new StringBuilder(str.length() + 30);

for(int i = 0, len = str.length(); i 

{

char c = str.charAt(i);

// 去除不可见字符

if((int)c 

{

continue;

}

switch(c)

{

case ‘

sb.append("<");

break;

case ‘>‘:

sb.append(">");

break;

case ‘&‘:

sb.append("&");

break;

case ‘"‘:

sb.append(""");

break;

case ‘\‘‘:

sb.append("'");

break;

case ‘/‘:

sb.append("/");

break;

default:

sb.append(c);

break;

}

}

str = null;

return sb.toString();

}

}

Velocity 提供的EventHandler接口需要在 velocity.properties中进行配置才能生效:

eventhandler.referenceinsertion.class = com.xxx.VelocityEscapeHtmlOutput

到此为止,就完成了对 Velocity 的 Reference 语法改造了!尽情享用吧!

原文:http://my.oschina.net/jsan/blog/340681

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值