Skip to content

Commit 0679671

Browse files
fix: Update URL validation regex
Fixes #7941
1 parent caecdb9 commit 0679671

5 files changed

Lines changed: 113 additions & 146 deletions

File tree

panel/src/helpers/url.isUrl.test.js

Lines changed: 8 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,61 +4,11 @@
44

55
import { describe, expect, it } from "vitest";
66
import url from "./url.js";
7+
import urls from "@/../../tests/Toolkit/fixtures/urls.json";
78

89
describe("$helper.url.isUrl", () => {
910
it("should detect URL in string", () => {
10-
// URL tests from our backend code in VTest.php
11-
const validUrls = [
12-
"http://www.getkirby.com",
13-
"http://www.getkirby.com/docs/param:value/?foo=bar/#anchor",
14-
"https://www.getkirby.de.vu",
15-
"https://getkirby.com:1234",
16-
"https://getkirby.com:1234/test",
17-
"http://foo.com/blah_blah",
18-
"http://foo.com/blah_blah/",
19-
"http://foo.com/blah_blah_(wikipedia)",
20-
"http://foo.com/blah_blah_(wikipedia)_(again)",
21-
"http://www.example.com/wpstyle/?p=364",
22-
"https://www.example.com/foo/?bar=baz&inga=42&quux",
23-
"http://✪df.ws/123",
24-
"http://userid:password@example.com:8080",
25-
"http://userid:password@example.com:8080/",
26-
"http://userid@example.com",
27-
"http://userid@example.com/",
28-
"http://userid@example.com:8080",
29-
"http://userid@example.com:8080/",
30-
"http://userid:password@example.com",
31-
"http://userid:password@example.com/",
32-
"http://142.42.1.1/",
33-
"http://142.42.1.1:8080/",
34-
"http://➡.ws/䨹",
35-
"http://⌘.ws",
36-
"http://⌘.ws/",
37-
"http://foo.com/blah_(wikipedia)#cite-1",
38-
"http://foo.com/blah_(wikipedia)_blah#cite-1",
39-
"http://foo.com/unicode_(✪)_in_parens",
40-
"http://foo.com/(something)?after=parens",
41-
"http://☺.damowmow.com/",
42-
"http://code.google.com/events/#&product=browser",
43-
"http://j.mp",
44-
"ftp://foo.bar/baz",
45-
"http://foo.bar/?q=Test%20URL-encoded%20stuff",
46-
"http://مثال.إختبار",
47-
"http://例子.测试",
48-
"http://उदाहरण.परीक्षा",
49-
"http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com",
50-
"http://1337.net",
51-
"http://a.b-c.de",
52-
"http://223.255.255.254",
53-
"http://localhost/test/",
54-
"http://localhost:8080/test",
55-
"http://127.0.0.1/kirby/",
56-
"http://127.0.0.1:8080/kirby",
57-
"https://127.0.0.1/kirby/panel/pages/blog+vvvv",
58-
"https://localhost/kirby/panel/pages/blog+vvvv"
59-
];
60-
61-
validUrls.forEach((testUrl) => {
11+
urls.valid.forEach((testUrl) => {
6212
expect(url.isUrl(testUrl, true)).toStrictEqual(true);
6313
});
6414

@@ -73,6 +23,12 @@ describe("$helper.url.isUrl", () => {
7323
expect(url.isUrl("javascript:alert(/XSS/)", true)).toStrictEqual(false);
7424
});
7525

26+
it("should reject invalid URLs in strict mode", () => {
27+
urls.invalid.forEach((testUrl) => {
28+
expect(url.isUrl(testUrl, true)).toStrictEqual(false);
29+
});
30+
});
31+
7632
it("should detect URL object", () => {
7733
expect(url.isUrl(new URL("https://getkirby.com"))).toStrictEqual(true);
7834
expect(url.isUrl(new URL("https://getkirby.com"), true)).toStrictEqual(

panel/src/helpers/url.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export function isUrl(url, strict) {
105105
// URL regex from the backend URL validator
106106
if (strict === true) {
107107
const regex =
108-
/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:localhost)|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i;
108+
/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:localhost)|(?:[a-z0-9\u00a1-\uffff](?:[a-z0-9\u00a1-\uffff_-]{0,62}[a-z0-9\u00a1-\uffff])?\.)+(?:[a-z\u00a1-\uffff]{2,}))(?::\d{2,5})?(?:[/?#]\S*)?$/i;
109109
return regex.test(url);
110110
}
111111

src/Toolkit/V.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,7 @@ public static function __callStatic(string $method, array $arguments): bool
632632
'url' => function ($value): bool {
633633
// In search for the perfect regular expression: https://mathiasbynens.be/demo/url-regex
634634
// Added localhost support and removed 127.*.*.* ip restriction
635-
$regex = '_^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:localhost)|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$_iu';
635+
$regex = '%^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:localhost)|(?:[a-z0-9\x{00a1}-\x{ffff}](?:[a-z0-9\x{00a1}-\x{ffff}_-]{0,62}[a-z0-9\x{00a1}-\x{ffff}])?\.)+(?:[a-z\x{00a1}-\x{ffff}]{2,}))(?::\d{2,5})?(?:[/?#]\S*)?$%iuS';
636636
return preg_match($regex, $value ?? '') !== 0;
637637
},
638638

tests/Toolkit/VTest.php

Lines changed: 10 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Exception;
66
use Kirby\Cms\App;
77
use Kirby\Content\Field;
8+
use Kirby\Data\Data;
89
use Kirby\Exception\InvalidArgumentException;
910
use Kirby\TestCase;
1011
use PHPUnit\Framework\Attributes\CoversClass;
@@ -675,98 +676,15 @@ public function testUuid(): void
675676

676677
public function testUrl(): void
677678
{
678-
// based on https://mathiasbynens.be/demo/url-regex
679-
$this->assertTrue(V::url('http://www.getkirby.com'));
680-
$this->assertTrue(V::url('http://www.getkirby.com/docs/param:value/?foo=bar/#anchor'));
681-
$this->assertTrue(V::url('https://www.getkirby.de.vu'));
682-
$this->assertTrue(V::url('https://getkirby.com:1234'));
683-
$this->assertTrue(V::url('https://getkirby.com:1234/test'));
684-
$this->assertTrue(V::url('http://foo.com/blah_blah'));
685-
$this->assertTrue(V::url('http://foo.com/blah_blah/'));
686-
$this->assertTrue(V::url('http://foo.com/blah_blah_(wikipedia)'));
687-
$this->assertTrue(V::url('http://foo.com/blah_blah_(wikipedia)_(again)'));
688-
$this->assertTrue(V::url('http://www.example.com/wpstyle/?p=364'));
689-
$this->assertTrue(V::url('https://www.example.com/foo/?bar=baz&inga=42&quux'));
690-
$this->assertTrue(V::url('http://✪df.ws/123'));
691-
$this->assertTrue(V::url('http://userid:password@example.com:8080'));
692-
$this->assertTrue(V::url('http://userid:password@example.com:8080/'));
693-
$this->assertTrue(V::url('http://userid@example.com'));
694-
$this->assertTrue(V::url('http://userid@example.com/'));
695-
$this->assertTrue(V::url('http://userid@example.com:8080'));
696-
$this->assertTrue(V::url('http://userid@example.com:8080/'));
697-
$this->assertTrue(V::url('http://userid:password@example.com'));
698-
$this->assertTrue(V::url('http://userid:password@example.com/'));
699-
$this->assertTrue(V::url('http://142.42.1.1/'));
700-
$this->assertTrue(V::url('http://142.42.1.1:8080/'));
701-
$this->assertTrue(V::url('http://➡.ws/䨹'));
702-
$this->assertTrue(V::url('http://⌘.ws'));
703-
$this->assertTrue(V::url('http://⌘.ws/'));
704-
$this->assertTrue(V::url('http://foo.com/blah_(wikipedia)#cite-1'));
705-
$this->assertTrue(V::url('http://foo.com/blah_(wikipedia)_blah#cite-1'));
706-
$this->assertTrue(V::url('http://foo.com/unicode_(✪)_in_parens'));
707-
$this->assertTrue(V::url('http://foo.com/(something)?after=parens'));
708-
$this->assertTrue(V::url('http://☺.damowmow.com/'));
709-
$this->assertTrue(V::url('http://code.google.com/events/#&product=browser'));
710-
$this->assertTrue(V::url('http://j.mp'));
711-
$this->assertTrue(V::url('ftp://foo.bar/baz'));
712-
$this->assertTrue(V::url('http://foo.bar/?q=Test%20URL-encoded%20stuff'));
713-
$this->assertTrue(V::url('http://مثال.إختبار'));
714-
$this->assertTrue(V::url('http://例子.测试'));
715-
$this->assertTrue(V::url('http://उदाहरण.परीक्षा'));
716-
$this->assertTrue(V::url("http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com"));
717-
$this->assertTrue(V::url('http://1337.net'));
718-
$this->assertTrue(V::url('http://a.b-c.de'));
719-
$this->assertTrue(V::url('http://223.255.255.254'));
720-
$this->assertTrue(V::url('http://localhost/test/'));
721-
$this->assertTrue(V::url('http://localhost:8080/test'));
722-
$this->assertTrue(V::url('http://127.0.0.1/kirby/'));
723-
$this->assertTrue(V::url('http://127.0.0.1:8080/kirby'));
724-
$this->assertTrue(V::url('https://127.0.0.1/kirby/panel/pages/blog+vvvv'));
725-
$this->assertTrue(V::url('https://localhost/kirby/panel/pages/blog+vvvv'));
726-
727-
// TODO: Find better regex to also cover the following
728-
// $this->assertTrue(V::url('http://special---offer.com/'));
729-
// $this->assertTrue(V::url('http://10.1.1.1'));
730-
// $this->assertTrue(V::url('http://10.1.1.254'));
731-
732-
$this->assertFalse(V::url('foo'));
733-
$this->assertFalse(V::url('http://'));
734-
$this->assertFalse(V::url('http://.'));
735-
$this->assertFalse(V::url('http://..'));
736-
$this->assertFalse(V::url('http://../'));
737-
$this->assertFalse(V::url('http://?'));
738-
$this->assertFalse(V::url('http://??'));
739-
$this->assertFalse(V::url('http://??/'));
740-
$this->assertFalse(V::url('http://#'));
741-
$this->assertFalse(V::url('http://##'));
742-
$this->assertFalse(V::url('http://##/'));
743-
$this->assertFalse(V::url('http://foo.bar?q=Spaces should be encoded'));
744-
$this->assertFalse(V::url('//'));
745-
$this->assertFalse(V::url('//a'));
746-
$this->assertFalse(V::url('///a'));
747-
$this->assertFalse(V::url('///'));
748-
$this->assertFalse(V::url('http:///a'));
749-
$this->assertFalse(V::url('foo.com'));
750-
$this->assertFalse(V::url('rdar://1234'));
751-
$this->assertFalse(V::url('h://test'));
752-
$this->assertFalse(V::url('http:// shouldfail.com'));
753-
$this->assertFalse(V::url(':// should fail'));
754-
$this->assertFalse(V::url('http://foo.bar/foo(bar)baz quux'));
755-
$this->assertFalse(V::url('ftps://foo.bar/'));
756-
$this->assertFalse(V::url('http://-error-.invalid/'));
757-
$this->assertFalse(V::url('http://a.b--c.de/'));
758-
$this->assertFalse(V::url('http://-a.b.co'));
759-
$this->assertFalse(V::url('http://a.b-.co'));
760-
$this->assertFalse(V::url('http://0.0.0.0'));
761-
$this->assertFalse(V::url('http://10.1.1.0'));
762-
$this->assertFalse(V::url('http://10.1.1.255'));
763-
$this->assertFalse(V::url('http://224.1.1.1'));
764-
$this->assertFalse(V::url('http://1.1.1.1.1'));
765-
$this->assertFalse(V::url('http://123.123.123'));
766-
$this->assertFalse(V::url('http://3628126748'));
767-
$this->assertFalse(V::url('http://.www.foo.bar/'));
768-
$this->assertFalse(V::url('http://www.foo.bar./'));
769-
$this->assertFalse(V::url('http://.www.foo.bar./'));
679+
$urls = Data::read(__DIR__ . '/fixtures/urls.json');
680+
681+
foreach ($urls['valid'] as $url) {
682+
$this->assertTrue(V::url($url), "Expected valid: $url");
683+
}
684+
685+
foreach ($urls['invalid'] as $url) {
686+
$this->assertFalse(V::url($url), "Expected invalid: $url");
687+
}
770688
}
771689

772690
public static function inputProvider(): array

tests/Toolkit/fixtures/urls.json

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{
2+
"based_on": "https://mathiasbynens.be/demo/url-regex",
3+
"valid": [
4+
"http://www.getkirby.com",
5+
"http://www.getkirby.com/docs/param:value/?foo=bar/#anchor",
6+
"https://www.getkirby.de.vu",
7+
"https://getkirby.com:1234",
8+
"https://getkirby.com:1234/test",
9+
"http://foo.com/blah_blah",
10+
"http://foo.com/blah_blah/",
11+
"http://foo.com/blah_blah_(wikipedia)",
12+
"http://foo.com/blah_blah_(wikipedia)_(again)",
13+
"http://www.example.com/wpstyle/?p=364",
14+
"https://www.example.com/foo/?bar=baz&inga=42&quux",
15+
"http://✪df.ws/123",
16+
"http://userid:password@example.com:8080",
17+
"http://userid:password@example.com:8080/",
18+
"http://userid@example.com",
19+
"http://userid@example.com/",
20+
"http://userid@example.com:8080",
21+
"http://userid@example.com:8080/",
22+
"http://userid:password@example.com",
23+
"http://userid:password@example.com/",
24+
"http://142.42.1.1/",
25+
"http://142.42.1.1:8080/",
26+
"http://➡.ws/䨹",
27+
"http://⌘.ws",
28+
"http://⌘.ws/",
29+
"http://foo.com/blah_(wikipedia)#cite-1",
30+
"http://foo.com/blah_(wikipedia)_blah#cite-1",
31+
"http://foo.com/unicode_(✪)_in_parens",
32+
"http://foo.com/(something)?after=parens",
33+
"http://☺.damowmow.com/",
34+
"http://code.google.com/events/#&product=browser",
35+
"http://j.mp",
36+
"ftp://foo.bar/baz",
37+
"http://foo.bar/?q=Test%20URL-encoded%20stuff",
38+
"http://مثال.إختبار",
39+
"http://例子.测试",
40+
"http://उदाहरण.परीक्षा",
41+
"http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com",
42+
"http://1337.net",
43+
"http://a.b-c.de",
44+
"http://223.255.255.254",
45+
"http://localhost/test/",
46+
"http://localhost:8080/test",
47+
"http://127.0.0.1/kirby/",
48+
"http://127.0.0.1:8080/kirby",
49+
"https://127.0.0.1/kirby/panel/pages/blog+vvvv",
50+
"https://localhost/kirby/panel/pages/blog+vvvv",
51+
"http://special---offer.com/"
52+
],
53+
"valid_unsolved": ["http://10.1.1.1", "http://10.1.1.254"],
54+
"invalid": [
55+
"foo",
56+
"foo.com",
57+
"http://",
58+
"http://.",
59+
"http://..",
60+
"http://../",
61+
"http://?",
62+
"http://??",
63+
"http://??/",
64+
"http://#",
65+
"http://##",
66+
"http://##/",
67+
"http://foo.bar?q=Spaces should be encoded",
68+
"//",
69+
"//a",
70+
"///a",
71+
"///",
72+
"http:///a",
73+
"rdar://1234",
74+
"h://test",
75+
"http:// shouldfail.com",
76+
":// should fail",
77+
"http://foo.bar/foo(bar)baz quux",
78+
"ftps://foo.bar/",
79+
"http://-error-.invalid/",
80+
"http://-a.b.co",
81+
"http://a.b-.co",
82+
"http://0.0.0.0",
83+
"http://10.1.1.0",
84+
"http://10.1.1.255",
85+
"http://224.1.1.1",
86+
"http://1.1.1.1.1",
87+
"http://123.123.123",
88+
"http://3628126748",
89+
"http://.www.foo.bar/",
90+
"http://www.foo.bar./",
91+
"http://.www.foo.bar./"
92+
]
93+
}

0 commit comments

Comments
 (0)