用于真实网址的URL验证正则表达式

6
我想验证给定的字符串是否为URL。在文本中匹配URL也很好,但不是必需的。我搜索和尝试了很多,但到目前为止我没有找到符合以下要求的内容:
1. 不能接受当处理成链接时存在安全风险的字符串。例如,<a href="javascript:alert(document.cookie)">clickme</a> 是一个有效的HTML元素,并且至少在某些浏览器中确实起作用(引发警报等)。我担心如果我允许任意方案(见下文),它可能会危及安全性(如此处所述:What is the best regular expression to check if a string is a valid URL?)。
2. 必须在JavaScript中正确工作。
3. 如果可以在Java中使用相同的代码就更好了——我正在开发GWT,这非常好但不是必需的。
4. 必须接受实际使用的URL,而不仅仅是符合标准的URL。具体例子包括:
a. 我希望接受http://fr.wikipedia.org/wiki/Français,这是非标准的,因为它包含了非英文字符,但是我的参考浏览器IE(7+)和Chrome接受它。
b. 我希望接受http://fr.wikipedia.org/wiki/Fran%c3%a7ais,这是非标准的,因为百分号编码应该是大写的,但是IE和Chrome也接受它。我想我可以进行大小写不敏感匹配——你能想到什么缺点吗?
c. 我希望接受http://localhost/localpath/servlet#action?param=value,这是非标准的,因为片段部分(从“#”到结尾)不应包括“?”和其他字符,但有些应用程序生成这样的URL并且浏览器接受它们。
d. 我希望接受具有任何协议/方案(不仅仅是http、https和ftp)的URL,因为我整合的各种应用程序及其用户可能需要传递此类URL。我可以禁止“javascript:”,并允许其他所有内容;如果您认为这会危及安全,请告诉我。
在SO和其他地方有很多关于这个主题的问题,但我没有找到一个正则表达式符合我所有的要求。例如:
  • GWT中用来匹配URL的正则表达式 -- 这是一个相当不错且简单的正则表达式,但是不能接受非标准的URL。我可以处理方案部分和百分号编码的大小写敏感性,但是无法解决其他问题。

  • https://dev59.com/NHVC5IYBdhLWcg3w21Iq#190405 -- 一个庞大的正则表达式(我自己都在想我使用的所有浏览器和框架是否都能处理这个大小),它似乎非常全面,但是声明符合标准,我却看不懂。

谢谢!:-)


1
嗯...那在服务器端验证地址怎么样?例如:1. 检查地址是否为http/https/ftp 2. 尝试从服务器ping该地址 3. 如果收到响应,则地址正确,否则向用户显示验证消息,说明地址无效。 - PrimosK
顺便说一下,您的浏览器确实会对URL字符串中的重音字符进行编码,只需将带重音的文本复制并粘贴到编辑器中即可。一些Web应用程序可以理解未经过URL编码的请求,但是途中的代理可能会拒绝。 - newtover
@PrimosK:我并没有检查URL是否是有效的意图。别管它需要额外的工作 - ping无法验证路径、查询等等,一个服务器可能对用户可访问但对我的服务器不可访问,而UI错误通知需要时间。 - Oren Shalev
@newtover:我知道,但这并不重要。我希望我的用户可以使用他们的浏览器所显示和接受的任何地址栏中的内容进行工作。 - Oren Shalev
请参考网站 http://mathiasbynens.be/demo/url-regex,以比较一些用于URL的正则表达式的示例。 - Mathias Bynens
显示剩余2条评论
2个回答

6
必须接受在实践中使用的URL,而不仅仅是符合标准的URL。
实际上,URI规范非常自由,允许一些通常需要排除的结构,以确保兼容性...
我希望接受 http://fr.wikipedia.org/wiki/Français,这是非标准的。
这不是一个URI,但它是一个相当标准的IRI
百分号编码应该是大写字母,因此非标准;从“#”到结尾的片段部分不应包括“?”,因此也是非标准的。但这两者都是符合URI标准的。RFC 3986建议创建百分数编码时使用大写字母,但并不要求如此。
我可以禁用'javascript:'并允许其他所有内容;如果您认为这会影响安全,请告诉我。
很遗憾,URI方案命名空间已经存在多个潜在危险的添加项,并且未来还将继续存在。此外,黑名单功能可能会被编码字符和控制字符所逃避。另外,任意方案匹配意味着您检测文本中的地址的次要目标将在大多数冒号使用时产生误报。白名单是唯一可行的方法,因此您必须逐个案例手动允许每个新方案。这需要一些小心;例如,“data:”方案似乎无害且有用,但可能存在与“javascript:”相同的XSS问题。您还需要了解每个方案的一些信息。“http”和“ftp”等方案具有“基于服务器的命名机构”,可以包括主机名和该主机内的资源路径;此外,您可能需要它们成为绝对URI。如果要允许文件URI,则必须检查它是否没有主机(“file:///”)。对于其他方案,URI标准本身可能不需要具体语法,但可能存在其他限制,例如“mailto:”必须使用有效的电子邮件地址。

巨大的正则表达式(我在想我使用的所有浏览器和框架是否都能处理这个大小),看起来非常全面。

这在JavaScript中不起作用,因为它具有不受支持的\x{code point}语法。此外,像JavaScript这样的语言,其正则表达式引擎是基于UTF-16代码单元而不是完整的Unicode代码点工作的,无法处理BMP之外的字符范围。

您必须使用类似\u00A0-\uFFFD的简单内容替换长的\x{A0}...\x{1FFFD}组,并单独检查无效的代理对以及0xnnFFFE-F非字符(如果您关心这些内容(可能不关心))。

可以说,在进行IRI验证之前,您可能已经在一般的输入扫描级别上剔除了任何错误的代理项和非字符;没有理由允许它们出现在任何文本输入中。将其放在单独的步骤中比试图将所有内容塞入单个正则表达式中更有意义。

替换后,引用的正则表达式中最长的部分是极长的数字检查字符串,尝试验证数值IP地址。这是正则表达式根本不擅长的事情。我强烈建议不要费心IPv6和IPv-future数字地址:即使假设很快普及IPv6采用率,可预见的未来也不会有人使用它们。(你甚至想允许链接到数字地址吗?取决于你的应用程序在做什么,但通常不需要。)

您还可以考虑禁止userinfo@主机名前缀(因为它们传统上除了欺骗攻击外没有任何用处),以及百分号编码的主机名(因为它们在Punycode存在的情况下没有任何作用,并且在某些浏览器中无法工作)。

因此,IRI验证没有单一的答案,但以下是您可能开始的地方:

(
    https?://
    (
        ([0-9]{1-3}(\.[0-9]{1-3}){3})|
        ([-0-9a-z\u00A0-\uFFFD]{1-63}(\.[-0-9a-z\u00A0-\uFFFD]{1-63})*)
    )
    (:[0-9]+)?/
    (
        %[0-9a-f][0-9a-f]|
        [-._!$&'()*+,:;=@~0-9a-z\u00A0-\uFFFD/?#]
    )*
)|(
    ftp://                                    // same again but with no ?query
    ...                                       // or port number
)|(
    mailto:                                   // specify requirements for
    ...                                       // other accepted schemes
)

(假设不区分大小写。这适用于DNS约束,而这些约束并不是URI规范本身的一部分,尽管它不完全地检查DNS标签中前导/尾随 - 或IPv4八位数的数字范围。验证电子邮件地址留给读者作为练习,因为这本身是一项艰巨的任务,如果您想严格执行它,则不适合使用正则表达式。)

哇,谢谢你这个棒极了的答案!我得仔细研究一下并决定如何继续,但即使第一次阅读也非常有启发性。 - Oren Shalev

1

由于您在服务器端使用Java,我建议您使用URI。它将接受您想要的所有“奇怪”内容,只需使用.getScheme()检查它是否确实是HTTP或HTTPS。

URL不同,URI不会尝试进行名称解析!


我会查看这个类,也许会添加服务器端验证,但我必须在客户端进行验证,以免让用户等待太久。谢谢。 - Oren Shalev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接