字符串翻转之谜

在内容安全审查中遇到一个奇特现象,用户评论中的敏感词无法被匹配。问题在于评论中的字符串顺序因Unicode控制字符而错乱。字符U+202E导致文本从右向左显示,而U+202C则结束翻转。这种特性使得常规字符串匹配方法失效。了解Unicode的控制字符后,通过在字符串前添加U+202E可实现翻转。在Hive中,只有U+202E生效。为了解决这个问题,可以创建UDF将实际显示的字符串转换出来,Java提供了内置工具进行处理。

做内容安全审查时,发现了一个奇怪的case。 明明用户的评论中含有敏感词,但是用关键词匹配却无法匹配成功。就好像like这个匹配方法失效了一样。

sql select '一个敏感词啊' like '%敏感词%' -- 结果竟然为false

用肉眼是无法看出问题的,把评论以文件形式下载到本地,用vim打开,才发现,原来文本的顺序是错乱的!见下图在这里插入图片描述
以这一段文本为例,<202d><202e>耳木<202c><202c>品质差别<202d><202e>大太<202c><202c>,看起来是木耳品质差别太大,但是存储的却是耳木大太。这样,用普通的方法,便是无法进行字符串匹配,从而逃过了安全审核。

这其实是unicode提供的一个功能,有一些unicode是控制字符。\u202e 的含义就是后面的字符翻转显示,\u202c 的含义是翻转控制到此结束。所以,在这两个控制字符之间的内容,全都是从右向左展示。

查询了一下,unicode相关的控制字符其实还有很多参考链接

开头结尾说明
\u2066\u2069之间的字符从左到右显示,不影响外围字符
\u2067\u2069之间的字符从右到左显示,不影响外围字符
\u2068\u2069之间的字符以第一个方向设定字符为准
\u202A\u202C之间的字符从左到右显示,可以影响外围字符
\u202B\u202C之间的字符从右到左显示,可以影响外围字符
\u202D\u202C之间的字符按照内存中的顺序,从左到右显示
\u202E\u202C之间的字符按照内存中的顺序,从右到左显示

也可以在这个网站查询unicode具体的含义https://www.fileformat.info/info/unicode/char/202e/index.htm

虽然unicode里面定义了,但是实现的实现却各有不同,有的可以生效,有的无法生效。在hive中只有\u202E 是生效的。

有了这些知识,翻转字符串变得简单了,只要在最前面加一个 \u202E 即可实现翻转。

select
	'hello world',
	'\u2067hello world\u2069',
	'\u202Bhello world\u202C',
	'\u202Ehello world'

这个sql只有最后一个hello world可以被翻转。

翻转字符带来很多麻烦,为了识别出实际展示的字符串,可以制作一个udf把实际展示的字符串翻译出来。java 本身已经内置了一个java.text.Bidi来方便进行处理。

import org.apache.hadoop.hive.ql.exec.UDF;
import org.apache.hadoop.io.Text;

import java.text.Bidi;

public final class StringNormalizeUDF extends UDF {

    public String evaluate(final String s) {
        if (s == null) {
            return null;
        }

        if (! Bidi.requiresBidi(s.toCharArray(), 0, s.length()) ){
            return s;
        }

        return visualToLogical(s);
    }

    private static String visualToLogical(String text) {
        if ((text == null) || (text.length() == 0)) {
            return text;
        }
    
        Bidi bidi = new Bidi(text, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT);
    
        if (bidi.isLeftToRight()) {
            return text;
        }
    
        int count = bidi.getRunCount();
        byte[] levels = new byte[count];
        Integer[] runs = new Integer[count];
    
        for (int i = 0; i < count; i++){
            levels[i] = (byte)bidi.getRunLevel(i);
            runs[i] = i;
        }
    
        Bidi.reorderVisually(levels, 0, runs, 0, count);
        StringBuilder result = new StringBuilder();

        for (int i = 0; i < count; i++) {
            int index = runs[i];
            int start = bidi.getRunStart(index);
            int end = bidi.getRunLimit(index);
            int level = levels[index];
    
            if ((level & 1) != 0) {
                for (; --end >= start;) {
                    result.append(text.charAt(end));
                }
            } else {
                result.append(text, start, end);
            }
        }

        return result.toString();
    }
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值