简析b站关于站内链接的正则匹配

前言

本文由 @Rorical 的文章《优雅的在哔哩哔哩评论区发送链接》启发,目的在于学习正则匹配的基本技巧。

本文将用介绍简单的正则匹配,并通过站内链接的正则匹配作为例子进行讲解。

阅读本文时可以参照: https://www.runoob.com/regexp/regexp-syntax.html 来帮助理解


什么是正则匹配

正则表达式(Regular Expression)是一种文本模式,包括普通字符(例如,a 到 z 之间的字母)和特殊字符(称为"元字符")。

正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串。

正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。

Credit: https://www.runoob.com/regexp/regexp-syntax.html

简单来说,正则匹配就是一串可以匹配具有特定格式的字符。

例如

runoo+b,可以匹配 runoob、runooob、runoooooob 等,+ 号代表前面的字符必须至少出现一次(1次或多次)。

runoob,可以匹配 runob、runoob、runoooooob 等, 号代表前面的字符可以不出现,也可以出现一次或者多次(0次、或1次、或多次)。

colou?r 可以匹配 color 或者 colour,? 问号代表前面的字符最多只可以出现一次(0次、或1次)。

Credit: https://www.runoob.com/regexp/regexp-syntax.html


Part1-专栏投稿中站内链接的正则匹配

站内链接-正则匹配-0.js

通过简单的搜索,可以发现专栏投稿中共有如下4条正则匹配规则(cdn的正则略过)

1.^(http:)?(https:)?(\/\/)?((([a-zA-Z0-9_-])+(\.)?){1,2}\.)?(bilibili.com)+(:\d+)?(\/((\.)?(\?)?=?&?%?[#!a-zA-Z0-9_-](\?)?)*)*$

2.^(http:)?(https:)?(\/\/)?(([a-zA-Z0-9_-])+(\.)?){0,2}(\.biligame.com)+(:\d+)?(\/((\.)?(\?)?=?&?%?[#!a-zA-Z0-9_-](\?)?)*)*$/i

3.^(http:)?(https:)?(\/\/)?(acg.tv)+(:\d+)?(\/((\.)?(\?)?=?&?%?[#!a-zA-Z0-9_-](\?)?)*)*$

4.^(bilibili:\/\/)(\S)+$

在这部分里,本文将由简单到复杂,以倒序的方式逐条进行说明。

^(bilibili:\/\/)(\S)+$

首先看几个基础组成部分

^ - 字符串必须由该正则匹配开始

$ - 字符串必须由该正则匹配结束

() - 代表一个子表达式

\ - 代表转义符号,跟在\后面的字符不代表任何含义 例如 \^ 就代表 ^, 使^失去了作用.

\S - 非打印字符,匹配任何非空白字符

+ - 匹配前面的子表达式一次或多次 (至少匹配前面的一次)

那么该表达式代表的意思就是

必需由 bilibili:// 开始,后面跟上至少一次或多次的任意字符。

例如 bilibili://oasidhjfjkasfnkhj

bilibili://oasidhjfjkasfnkhj

无法匹配的,比如不是由bilibili://开头的, abilibili://adfasdfasf

abilibili://adfasdfasf


^(http:)?(https:)?(\/\/)?(acg.tv)+(:\d+)?(\/((\.)?(\?)?=?&?%?[#!a-zA-Z0-9_-](\?)?)*)*$

同样先把基础部分标一下

? - 代表匹配0次或者一次 (最多匹配一次)

\d - 代表匹配数字

[] - 代表可以匹配的字符,例如[0123456789]代表可以匹配任意一个数字,[0123456789]等同于[0-9]等同于\d

然后是是解析

(http:)?(https:)?(\/\/)? 代表至多匹配 http: , https: , // 各1次

(acg.tv)+ 代表匹配 acg.tv 至少一次

(:\d+)? 代表 :数字 例如 :80 :99(其实就是端口号),出现0次或者1次,

(\/((\.)?(\?)?=?&?%?[#!a-zA-Z0-9_-](\?)?)*)*$ 代表 匹配以/开头的任意字符或者干脆没有(讲起来太麻烦了)

该正则的本意是匹配例如 acg.tv/BV1oA411v7fp 的网址,但实际上却可以匹配很多根本不是网址的字符串。

比如http: 那里只写了至多能匹配一次,没写互斥。acg.tv只写了至少匹配一次.所以可能会出现如下情况

acg.tv bypass 1

可能的解决方式: ^((http(s)?:\/\/)|(\/\/))?(acg.tv)(:\d+)?(\/((\.)?(\?)?=?&?%?[#!a-zA-Z0-9_-](\?)?)*)*$


^(http:)?(https:)?(\/\/)?(([a-zA-Z0-9_-])+(\.)?){0,2}(\.biligame.com)+(:\d+)?(\/((\.)?(\?)?=?&?%?[#!a-zA-Z0-9_-](\?)?)*)*$

重复的不再说明了

{n,m} -  m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。

(([a-zA-Z0-9_-])+(\.)?){0,2} 代表 (匹配任意字符最少一次+0个或者1个点),并且该子表达式最多匹配两次最少不匹配。

比如 a.a. , ab.a , a. 等可以都匹配。但是 a.a.a.就不行了。说白了就是想要匹配子域名,且最多匹配到3级子域名。

该正则的本意是匹配biligame.com旗下的网站已经其2/3级子域名。

例如 https://www.biligame.com/detail/?id=101772

但是实际怎么就不用我多说了吧。先不提前文提到的http:

其他的比如

a.a..biligame.com 都可以出现超链接。

biligame bypass 1

还有更奇妙的是

biligame.com 居然是非法链接。

我人傻了

可能的解决方式: ^((http(s)?:\/\/)|(\/\/))?([a-zA-Z0-9_-]+\.){0,2}(biligame.com)(:\d+)?(\/((\.)?(\?)?=?&?%?[#!a-zA-Z0-9_-](\?)?)*)*$


^(http:)?(https:)?(\/\/)?((([a-zA-Z0-9_-])+(\.)?){1,2}\.)?(bilibili.com)+(:\d+)?(\/((\.)?(\?)?=?&?%?[#!a-zA-Z0-9_-](\?)?)*)*$

这个没啥新的好说了,基本和上一个一致

bilibili.com bypass 1

可能的解法: ^((http(s)?:\/\/)|(\/\/))?([a-zA-Z0-9_-]+\.){0,2}(bilibili.com)(:\d+)?(\/((\.)?(\?)?=?&?%?[#!a-zA-Z0-9_-](\?)?)*)*$


Part2-评论区中站内链接的正则匹配

详情参照@Rorical 的文章《优雅的在哔哩哔哩评论区发送链接》

评论区有个功能,就是自动把b站的官方链接直接变成超链接(蓝链),这样就可以直接点链接跳转了。

判断是否为官方链接的代码依旧写死在commet.js里。

regex in commet

(http(s)?:\/\/)?([a-z0-9A-Z]+.)?(bilibili.(com|tv|cn)|biligame.(com|cn)|(bilibiliyoo|im9).com|biliapi.net|b23.tv|sugs.suning.com|kaola.com)(\$|\/|)([\/.$*?~=#!%@&-A-Za-z0-9_]*)(?![^<>]*>|[^"]*?<\/a)

. - 代表任意字符

先说几个没问题的

(http(s)?:\/\/)? 匹配http://或者https:// 最多一次

([/.$*?~=#!%@&-A-Za-z0-9_]*)(?![^<>]*>|[^"]*?<\/a) 代表匹配任意字符

然后就是有问题的

(\$|\/|) 代表匹配 $ 或者 / 或者 空值, 但是谁会去匹配一个 $ 啊,外星人嘛?

该表达式的本意是要么有/匹配后面路径,要么没有/,不匹配后面路径。

但是不知道哪个弱智程序员居然把$转义了,导致$根本不能起到本应该有的作用。

另外 这边还有另外一个错误,在后面会用到,就是 | 了一个空值,就是说啥都不匹配也行。

这段就直接相当于 (\$|\/)? 这又加个bug。

接下来就是大问题了

包括([a-z0-9A-Z]+.)?(bilibili.(com|tv|cn)|biligame.(com|cn)|(bilibiliyoo|im9).com|biliapi.net|b23.tv|sugs.suning.com|kaola.com)在内的所有.都没有转义

就拿第一个来说

([a-z0-9A-Z]+.)? 匹配一串字符+一个 . ,最多一次。

同时,因为 . 代表任意字符, 所以该子表达式就几乎等同于 ([a-z0-9A-Z]{1,})?

也就是说,要么匹配两个以上的任意字符,要么不匹配。

但是该子表达式的本意匹配一个二级域名或者不匹配二级域名, .的不转义导致该表达式失去效果。

commet bypass 2

这里,可以直接用一级域名做个跳转就能实现蓝链接。

再看下一段,同样是.没有转义。举个例子 bilibili.com, 那么符合规则的就不止一个bilibili.com了 bilibili0com bilibiliacom bilibilibcom bilibili0com 都是可以的

commet bypass 3

这个时候,利用这边的漏洞以及上面一个可以不匹配的漏洞,可以轻松构造出一个能够被匹配的二级域名。

commet bypass 4 aabilibili0com.a.a/fadfasd?a=afda

这样子的话,用二级域名直接做个跳转就能实现蓝链接跳转了。

当然照惯例,给出修复方法: (http(s)?:\/\/)?([a-z0-9A-Z]+\.)?(bilibili\.(com|tv|cn)|biligame\.(com|cn)|(bilibiliyoo|im9)\.com|biliapi\.net|b23\.tv|sugs\.suning\.com|kaola\.com)($|\/)([/.$*?~=#!%@&-A-Za-z0-9_]*)(?![^<>]*>|[^"]*?<\/a)


Part3-进阶-后端正则匹配的猜解

这段内容中,本文将简单介绍如何猜解b23.tv短链接后端中的正则匹配。

POST /x/share/click?build=9333&buvid=db234615f49c5ca155cc50d6c04bb700&oid=https://www.bilibili.com&platform=ios&share_channel=COPY&share_content=123&share_id=public.webview.0.0.pv&share_mode=1&share_origin=&share_title=123&sid= HTTP/1.1
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 66c47e9c-74d6-4a48-933c-12fcdd8d4f22
Host: api.bilibili.com
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: 
Content-Length: 0

尝试 https://www.bilibili.com -> 成功

尝试 https://www.biligame.com -> 成功

尝试 https://www.im9.com -> 成功

尝试 https://www.biliapi.net -> 成功

尝试 https://www.bilibili.co -> 成功

尝试 https://aabilibili0com -> 成功, 说明没有转义.

可以首先构建出一段子表达式(bilibili.com|biligame.com|im9.com|biliapi.net|bilibili.co)

尝试 https://www.bilibili.com -> 成功

尝试 https://a.a.a.a.a.bilibili.com -> 成功

尝试 www.bilibili.com -> 失败

尝试 https://bilibili.com -> 失败

尝试 http:https://www.bilibili.com -> 失败

由此可以推断出,后端的正则匹配必须有http或者https以及一个二级域名

结合上一段,构建出子表达式

^(http(s)?:\/\/)([a-z0-9A-Z]+.)+(bilibili.com|biligame.com|im9.com|biliapi.net|bilibili.co)

接下来尝试2级域名

尝试 https://aabilibili0com.a.a -> 失败

尝试 https://aabilibili0com/.a.a -> 成功

尝试 https://aabilibili0com -> 成功

尝试 https://aabilibili0com/ -> 成功

由此构建子表达式

($|\/)([\/.$*?~=#!%@&-A-Za-z0-9_]*)(?![^<>]*>|[^"]*?<\/a)

结合起来 推测后端正则表达式为

^(http(s)?:\/\/)([a-z0-9A-Z]+.)+(bilibili.com|biligame.com|im9.com|biliapi.net|bilibili.co)($|\/)([\/.$*?~=#!%@&-A-Za-z0-9_]*)(?![^<>]*>|[^"]*?<\/a)

那么,要绕过后端只有一种办法,就是买个域名。例如

https://www.aabilibili.com
https://www.asd123fbilibili.com
https://www.aaim9.com
https://www.1254im9.com
https://www.vcxzdfaim9.com
https://www.123fgadsbilibili.com
https://www.bsfdvbsbilibili.com

解决办法类似评论区

^(http(s)?:\/\/)([a-z0-9A-Z]+\.)+(bilibili\.com|biligame\.com|im9\.com|biliapi\.net|bilibili\.co)($|\/)([\/.$*?~=#!%@&-A-Za-z0-9_]*)(?![^<>]*>|[^"]*?<\/a)