mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-07-16 05:33:49 +03:00
Compare commits
483 Commits
MilnerMart
...
v26.03.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfeb035548 | ||
|
|
66dcd1f7a8 | ||
|
|
ef82119226 | ||
|
|
1b9ec75903 | ||
|
|
d1bde2fc78 | ||
|
|
16562816c5 | ||
|
|
cf648906e9 | ||
|
|
fddeb9030b | ||
|
|
99a704698d | ||
|
|
fc220dea39 | ||
|
|
82ef7356f3 | ||
|
|
cb6c5d71a0 | ||
|
|
508cf0ade6 | ||
|
|
851aba228a | ||
|
|
7111e080c1 | ||
|
|
ee4786f83a | ||
|
|
33d2eee4b2 | ||
|
|
89caab1108 | ||
|
|
446b4a7d3d | ||
|
|
d335b49be0 | ||
|
|
d9b9303a42 | ||
|
|
50a7183b32 | ||
|
|
1db1083064 | ||
|
|
664eb6d980 | ||
|
|
7528bc19b7 | ||
|
|
6854687d7c | ||
|
|
ad540a015f | ||
|
|
f54f507854 | ||
|
|
f484fbc110 | ||
|
|
299d3b3149 | ||
|
|
7abc269316 | ||
|
|
f0cf4bd0f8 | ||
|
|
ed4baed28c | ||
|
|
90d011fc15 | ||
|
|
805fd98c0f | ||
|
|
fcbae16730 | ||
|
|
7c3a4c7e85 | ||
|
|
114fa802c0 | ||
|
|
8fcd3b24b3 | ||
|
|
ce703403c2 | ||
|
|
16110273ff | ||
|
|
93bcbd168e | ||
|
|
46001d61d0 | ||
|
|
8dd238ceae | ||
|
|
bb7fd59de9 | ||
|
|
ad8fc95521 | ||
|
|
cca066a258 | ||
|
|
bbda5fd468 | ||
|
|
8429cc93eb | ||
|
|
fef61f054a | ||
|
|
8082c95ec3 | ||
|
|
fcabf478de | ||
|
|
8de2c28497 | ||
|
|
0838d5ea16 | ||
|
|
449ac40114 | ||
|
|
3131050acd | ||
|
|
c0d2874892 | ||
|
|
5940a91809 | ||
|
|
9a4651badb | ||
|
|
92d15d9cf2 | ||
|
|
b06147fef7 | ||
|
|
841350a937 | ||
|
|
12183bac07 | ||
|
|
e65b4b63a2 | ||
|
|
7cac3f4780 | ||
|
|
92cd11d105 | ||
|
|
13115ace84 | ||
|
|
73f9834e6f | ||
|
|
3afe855156 | ||
|
|
bfde896f0b | ||
|
|
1cdc0a7a3d | ||
|
|
d19b86640b | ||
|
|
2936ba609b | ||
|
|
573a2dd22a | ||
|
|
b55cc803d3 | ||
|
|
304ade418e | ||
|
|
997931c42f | ||
|
|
268e353431 | ||
|
|
b491b5fbca | ||
|
|
387c786768 | ||
|
|
2641586a6f | ||
|
|
6d2cd20e80 | ||
|
|
b0c574356a | ||
|
|
07e45a20e5 | ||
|
|
14056c69e6 | ||
|
|
fb9c840c46 | ||
|
|
5fba4a5399 | ||
|
|
c0b377050e | ||
|
|
f3efb6441d | ||
|
|
0cf313a21e | ||
|
|
26aadffb20 | ||
|
|
a5f48e3202 | ||
|
|
b0dda6e6a7 | ||
|
|
d4025d95e7 | ||
|
|
d6021f4d22 | ||
|
|
b9a3290731 | ||
|
|
48f235ea5a | ||
|
|
047771b9f4 | ||
|
|
b5375114d3 | ||
|
|
fc13e56cea | ||
|
|
77fc37ac25 | ||
|
|
3424351e84 | ||
|
|
606f9d92d0 | ||
|
|
a5e25abb9c | ||
|
|
b310e87e4c | ||
|
|
425baf9d6e | ||
|
|
825c369ad9 | ||
|
|
10bab70438 | ||
|
|
350e0b281b | ||
|
|
08805ea3c8 | ||
|
|
9441e32c69 | ||
|
|
530fc37067 | ||
|
|
369e499dce | ||
|
|
655815de6d | ||
|
|
457adc1fee | ||
|
|
e86a90967e | ||
|
|
5d08f7cf14 | ||
|
|
8744eb2d62 | ||
|
|
d8383cfa80 | ||
|
|
4626278447 | ||
|
|
c61af9c22b | ||
|
|
72521d0906 | ||
|
|
7e44b195c5 | ||
|
|
5b45eac5e1 | ||
|
|
c1d30341e7 | ||
|
|
80d2b4913b | ||
|
|
3f473528b1 | ||
|
|
d0dcd4f61b | ||
|
|
bde66a1396 | ||
|
|
4de5a2d9bf | ||
|
|
27bf4299cf | ||
|
|
164f01bb25 | ||
|
|
f563a005f5 | ||
|
|
a14d8e30cc | ||
|
|
a9194ffb63 | ||
|
|
2f9c1b7127 | ||
|
|
bbea76668b | ||
|
|
becc630acf | ||
|
|
4ac8ecad6b | ||
|
|
903e88c700 | ||
|
|
ed96aa820e | ||
|
|
63ec079b7b | ||
|
|
d485fcb3db | ||
|
|
0f895668a4 | ||
|
|
6c577ac3bf | ||
|
|
31cc2423d2 | ||
|
|
c9ed32e518 | ||
|
|
6b4c3a0969 | ||
|
|
2dad92d1bd | ||
|
|
c1fb7ab7dc | ||
|
|
98315f3899 | ||
|
|
8c82aaabd6 | ||
|
|
ce9b536b78 | ||
|
|
d9c50e5bc1 | ||
|
|
bf075f7dd8 | ||
|
|
a4fd673285 | ||
|
|
e794c977bc | ||
|
|
0b088ef1d3 | ||
|
|
bf6a6af683 | ||
|
|
914790fd99 | ||
|
|
edb0c6a9e8 | ||
|
|
84049de696 | ||
|
|
da0531e63b | ||
|
|
421dc75f4e | ||
|
|
8ae91df038 | ||
|
|
64b41dd626 | ||
|
|
ebd6e4d3a2 | ||
|
|
80374aea5c | ||
|
|
2ac9efae7d | ||
|
|
a11d565ba4 | ||
|
|
1fdf854ea7 | ||
|
|
e9c9792cb9 | ||
|
|
5ae524c25a | ||
|
|
0d7287fc8b | ||
|
|
e77c96f6b7 | ||
|
|
9b8a10dd3a | ||
|
|
49200ca5ce | ||
|
|
34aa4dbf10 | ||
|
|
5ee79d16c9 | ||
|
|
a1ea4006e0 | ||
|
|
9078188939 | ||
|
|
ed0aad1a7a | ||
|
|
5c59cfb020 | ||
|
|
3ca15ad68a | ||
|
|
60014989f5 | ||
|
|
57b10f195e | ||
|
|
b1e95eb39f | ||
|
|
b3da77b8f9 | ||
|
|
1a345b74bb | ||
|
|
8ffc3a4abf | ||
|
|
7233c1c7b2 | ||
|
|
1309a01131 | ||
|
|
0333185b6d | ||
|
|
83f89f64e8 | ||
|
|
11a1a6fb16 | ||
|
|
882c609296 | ||
|
|
176a0dcd59 | ||
|
|
94b0f70bfa | ||
|
|
08b2a77d41 | ||
|
|
3e8e9a23cf | ||
|
|
58b83b64c8 | ||
|
|
dfe4cde6ee | ||
|
|
d11144d9e2 | ||
|
|
f96b0ea5f3 | ||
|
|
815f8d79ed | ||
|
|
b62dab32e0 | ||
|
|
262f863981 | ||
|
|
a4c94390a1 | ||
|
|
53f3cca85d | ||
|
|
ed08bbcecc | ||
|
|
de97ebf9b7 | ||
|
|
f492a660a8 | ||
|
|
09436836a5 | ||
|
|
bb455d7788 | ||
|
|
009212ab80 | ||
|
|
ba9cb591c8 | ||
|
|
d00ac2f34e | ||
|
|
bd4dc6d463 | ||
|
|
d91180a909 | ||
|
|
bc2913a5cb | ||
|
|
4802394562 | ||
|
|
1755556468 | ||
|
|
01cdbdb7ae | ||
|
|
fc8bbf3eab | ||
|
|
3cdab19319 | ||
|
|
5661d20e87 | ||
|
|
91f80123e8 | ||
|
|
7a0636d0f8 | ||
|
|
0fe5bdfbac | ||
|
|
f88687e977 | ||
|
|
68d437d05b | ||
|
|
1e56aaea04 | ||
|
|
dab170a6fe | ||
|
|
a8de717d9b | ||
|
|
78fe95b6fc | ||
|
|
e0c24e41aa | ||
|
|
fa8553839b | ||
|
|
b8fcefc794 | ||
|
|
88bcb68fcb | ||
|
|
7c000553ae | ||
|
|
391fa35c80 | ||
|
|
c6773a8c9f | ||
|
|
9b226e7d39 | ||
|
|
9865446267 | ||
|
|
926abbe776 | ||
|
|
4fabef3a57 | ||
|
|
5ef4cd80c3 | ||
|
|
e01f23583f | ||
|
|
7792cb3915 | ||
|
|
be26253a18 | ||
|
|
1bdd1f8189 | ||
|
|
fa62c79b17 | ||
|
|
d7d8fa1e5b | ||
|
|
18562f1e10 | ||
|
|
86090a694f | ||
|
|
1ee8287c73 | ||
|
|
8eb98cd591 | ||
|
|
0f9ba21b05 | ||
|
|
834f8e7046 | ||
|
|
32e3399334 | ||
|
|
2d8698a218 | ||
|
|
454fb883a2 | ||
|
|
6f4a6ab8ea | ||
|
|
9c4b6f36f1 | ||
|
|
78886b1e67 | ||
|
|
d9debaf032 | ||
|
|
d4360d6347 | ||
|
|
175b1785c0 | ||
|
|
c8740c0171 | ||
|
|
91ee895a74 | ||
|
|
a045e46571 | ||
|
|
44eaa65c3b | ||
|
|
0a22af7b14 | ||
|
|
b54702ab08 | ||
|
|
c4fdcfc5d1 | ||
|
|
cb8117e8df | ||
|
|
5a218d5056 | ||
|
|
8dbc5cf9c6 | ||
|
|
71e81615a3 | ||
|
|
611d37da04 | ||
|
|
0e799a3857 | ||
|
|
b91d6e2bfa | ||
|
|
ea16ad7e94 | ||
|
|
ba6eb54552 | ||
|
|
f705e7683b | ||
|
|
dc996adb20 | ||
|
|
a64c638ccc | ||
|
|
359c067279 | ||
|
|
66a746e297 | ||
|
|
a4d43ee24b | ||
|
|
f7793a70a9 | ||
|
|
ceba3d31fb | ||
|
|
eecc08edde | ||
|
|
eb19aadc75 | ||
|
|
06c81e69b9 | ||
|
|
3dc3d4a639 | ||
|
|
94c59c1e3d | ||
|
|
4d2205853a | ||
|
|
751772b87a | ||
|
|
76e30869e1 | ||
|
|
3edc9fe9eb | ||
|
|
616c62703e | ||
|
|
ecd56917e7 | ||
|
|
e22c9cae91 | ||
|
|
29ddb6e1b9 | ||
|
|
2ff90e2ff0 | ||
|
|
04ecc128a2 | ||
|
|
87d1d3423b | ||
|
|
4818192a2a | ||
|
|
965dd97f54 | ||
|
|
195b74926c | ||
|
|
2120db12b2 | ||
|
|
ed563fef28 | ||
|
|
0d31a8e3f1 | ||
|
|
b8354b974b | ||
|
|
034c1e289d | ||
|
|
f31605a3de | ||
|
|
e7cc75c74d | ||
|
|
4b79d5e4e8 | ||
|
|
34854915b3 | ||
|
|
af6f34b529 | ||
|
|
fb82a2b896 | ||
|
|
5b464938b6 | ||
|
|
81f954890d | ||
|
|
0e2bbcec62 | ||
|
|
fdd339f525 | ||
|
|
8cf7d6a83d | ||
|
|
58a5008718 | ||
|
|
c44a8df55d | ||
|
|
ff1494c519 | ||
|
|
b8ce8fd852 | ||
|
|
75e7454a5f | ||
|
|
2558ea8931 | ||
|
|
ac0f47a4b2 | ||
|
|
4f16129869 | ||
|
|
64a8037fdd | ||
|
|
7502ba1bc8 | ||
|
|
33a04697ef | ||
|
|
b70a5c0cdb | ||
|
|
9443ae9f40 | ||
|
|
220c2a4102 | ||
|
|
e9914eb301 | ||
|
|
934512d09c | ||
|
|
9102c90986 | ||
|
|
c3e74219c4 | ||
|
|
13c9d7bc2d | ||
|
|
119b539586 | ||
|
|
29a5c180f0 | ||
|
|
7906602291 | ||
|
|
6dafe773ff | ||
|
|
25bc28a1be | ||
|
|
4c561c7fa0 | ||
|
|
95b3e78573 | ||
|
|
63a345bc93 | ||
|
|
e093a172cb | ||
|
|
4b01f8934b | ||
|
|
bc116b45b5 | ||
|
|
a059960b9e | ||
|
|
7770966fed | ||
|
|
d7adcf6c69 | ||
|
|
04a364dcc3 | ||
|
|
db83ac7eaa | ||
|
|
3ca9dddf61 | ||
|
|
bf74f53ca7 | ||
|
|
9d67efb4a4 | ||
|
|
3a39b9f440 | ||
|
|
27f7aab375 | ||
|
|
337da0c467 | ||
|
|
f56b3560c4 | ||
|
|
02dfe11ce6 | ||
|
|
83d06beb70 | ||
|
|
a8cfc059c8 | ||
|
|
1614b2bab0 | ||
|
|
4bdec0d214 | ||
|
|
6a7d7e7c2b | ||
|
|
30d4674657 | ||
|
|
9f961f95f8 | ||
|
|
bab99a26ec | ||
|
|
9a7fecd269 | ||
|
|
a8dc0d449b | ||
|
|
a0381f76bf | ||
|
|
6102f66daa | ||
|
|
c6134d162d | ||
|
|
2046f9b9de | ||
|
|
ac3ba594a4 | ||
|
|
22df25a480 | ||
|
|
8b30c7f02e | ||
|
|
757cdddc7c | ||
|
|
df95e99680 | ||
|
|
5a6d544db7 | ||
|
|
16117d329c | ||
|
|
e90da18ada | ||
|
|
a08d80e1cc | ||
|
|
6258175922 | ||
|
|
15736777a0 | ||
|
|
75915e8a94 | ||
|
|
9bde0ae4ea | ||
|
|
0c802d1f86 | ||
|
|
b7a96c6466 | ||
|
|
4b645a82c7 | ||
|
|
d599b77b6f | ||
|
|
26e93dc8c1 | ||
|
|
a4c9a8491b | ||
|
|
70ee636d87 | ||
|
|
b35f6dbb03 | ||
|
|
67d9e24d8f | ||
|
|
3903fda6ca | ||
|
|
441e46ebaa | ||
|
|
1f4260f359 | ||
|
|
dc0bf8ad4e | ||
|
|
102e326e6a | ||
|
|
2b25bf6f3b | ||
|
|
f93280696d | ||
|
|
1787391b07 | ||
|
|
a74a8ee483 | ||
|
|
7fa5405cb7 | ||
|
|
6725ddcc41 | ||
|
|
bce941db3f | ||
|
|
6d926048ec | ||
|
|
5335c973b4 | ||
|
|
15c3e5c96e | ||
|
|
a5d5904969 | ||
|
|
598758b991 | ||
|
|
9926e23bc8 | ||
|
|
5d3264bc63 | ||
|
|
d71f819f95 | ||
|
|
ee13509760 | ||
|
|
82d7bb1f32 | ||
|
|
cdfda508d8 | ||
|
|
da941e584f | ||
|
|
65874d7b96 | ||
|
|
ac9b8f405c | ||
|
|
8d1419a12e | ||
|
|
04f7a7d301 | ||
|
|
c10d2a1493 | ||
|
|
97bbf79ffd | ||
|
|
f7b01ae53d | ||
|
|
d704e1dbba | ||
|
|
ef2ff5e093 | ||
|
|
7caed3b0db | ||
|
|
45641d0754 | ||
|
|
4b1d08ba99 | ||
|
|
160fa99ba4 | ||
|
|
d2a5ab49ed | ||
|
|
c6404d8917 | ||
|
|
7113807f12 | ||
|
|
be711215e8 | ||
|
|
7e3b404240 | ||
|
|
e86901ca20 | ||
|
|
bdfa61c8b2 | ||
|
|
2cc36787f5 | ||
|
|
448ac61b48 | ||
|
|
753f6394f7 | ||
|
|
b1faf65934 | ||
|
|
09f478bd74 | ||
|
|
a0497feddd | ||
|
|
789693bde9 | ||
|
|
1fe933e4ea | ||
|
|
724b4b5a70 | ||
|
|
1778a56146 | ||
|
|
744865fcb2 | ||
|
|
7f8c8b448d | ||
|
|
a67c53826d | ||
|
|
14b131e850 | ||
|
|
9b55a52b85 | ||
|
|
db1d10e80f | ||
|
|
1be576966f | ||
|
|
b97e792c5f | ||
|
|
8dec674cc3 | ||
|
|
f784c03746 | ||
|
|
148e172fe8 | ||
|
|
56ae86646f | ||
|
|
1d2b6fdfa2 | ||
|
|
4fc75beed4 | ||
|
|
3b3bc0c4bf | ||
|
|
910faab88e | ||
|
|
f184d763ad | ||
|
|
a91d42634d | ||
|
|
f517ef3616 | ||
|
|
e99507ddcf | ||
|
|
d2cacf1945 | ||
|
|
448ac1405b | ||
|
|
6ad21ce885 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -8,10 +8,10 @@ Homestead.yaml
|
||||
.idea
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/public/dist
|
||||
/public/dist/*.map
|
||||
/public/plugins
|
||||
/public/css
|
||||
/public/js
|
||||
/public/css/*.map
|
||||
/public/js/*.map
|
||||
/public/bower
|
||||
/public/build/
|
||||
/public/favicon.ico
|
||||
|
||||
@@ -22,4 +22,10 @@ trait HandlesPartialLogins
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
protected function clearLastAttemptedUser(): void
|
||||
{
|
||||
$loginService = app()->make(LoginService::class);
|
||||
$loginService->clearLastLoginAttempted();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use BookStack\Access\LoginService;
|
||||
use BookStack\Access\Mfa\BackupCodeService;
|
||||
use BookStack\Access\Mfa\MfaSession;
|
||||
use BookStack\Access\Mfa\MfaValue;
|
||||
use BookStack\Access\Mfa\MfaVerificationLimiter;
|
||||
use BookStack\Activity\ActivityType;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Http\Controller;
|
||||
@@ -19,6 +20,11 @@ class MfaBackupCodesController extends Controller
|
||||
|
||||
protected const SETUP_SECRET_SESSION_KEY = 'mfa-setup-backup-codes';
|
||||
|
||||
public function __construct(
|
||||
protected MfaVerificationLimiter $limiter,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a view that generates and displays backup codes.
|
||||
*/
|
||||
@@ -71,6 +77,12 @@ class MfaBackupCodesController extends Controller
|
||||
public function verify(Request $request, BackupCodeService $codeService, MfaSession $mfaSession, LoginService $loginService)
|
||||
{
|
||||
$user = $this->currentOrLastAttemptedUser();
|
||||
$this->limiter->incrementAttempts($user, $request);
|
||||
if ($this->limiter->hasHitLimit($user, $request)) {
|
||||
$this->clearLastAttemptedUser();
|
||||
$this->limiter->throwException();
|
||||
}
|
||||
|
||||
$codes = MfaValue::getValueForUser($user, MfaValue::METHOD_BACKUP_CODES) ?? '[]';
|
||||
|
||||
$this->validate($request, [
|
||||
@@ -89,6 +101,7 @@ class MfaBackupCodesController extends Controller
|
||||
|
||||
$mfaSession->markVerifiedForUser($user);
|
||||
$loginService->reattemptLoginFor($user);
|
||||
$this->limiter->decrementAttempts($user, $request);
|
||||
|
||||
if ($codeService->countCodesInSet($updatedCodes) < 5) {
|
||||
$this->showWarningNotification(trans('auth.mfa_backup_codes_usage_limit_warning'));
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace BookStack\Access\Controllers;
|
||||
use BookStack\Access\LoginService;
|
||||
use BookStack\Access\Mfa\MfaSession;
|
||||
use BookStack\Access\Mfa\MfaValue;
|
||||
use BookStack\Access\Mfa\MfaVerificationLimiter;
|
||||
use BookStack\Access\Mfa\TotpService;
|
||||
use BookStack\Access\Mfa\TotpValidationRule;
|
||||
use BookStack\Activity\ActivityType;
|
||||
@@ -20,7 +21,8 @@ class MfaTotpController extends Controller
|
||||
protected const SETUP_SECRET_SESSION_KEY = 'mfa-setup-totp-secret';
|
||||
|
||||
public function __construct(
|
||||
protected TotpService $totp
|
||||
protected TotpService $totp,
|
||||
protected MfaVerificationLimiter $limiter,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -86,6 +88,12 @@ class MfaTotpController extends Controller
|
||||
public function verify(Request $request, LoginService $loginService, MfaSession $mfaSession)
|
||||
{
|
||||
$user = $this->currentOrLastAttemptedUser();
|
||||
$this->limiter->incrementAttempts($user, $request);
|
||||
if ($this->limiter->hasHitLimit($user, $request)) {
|
||||
$this->clearLastAttemptedUser();
|
||||
$this->limiter->throwException();
|
||||
}
|
||||
|
||||
$totpSecret = MfaValue::getValueForUser($user, MfaValue::METHOD_TOTP);
|
||||
|
||||
$this->validate($request, [
|
||||
@@ -98,6 +106,7 @@ class MfaTotpController extends Controller
|
||||
|
||||
$mfaSession->markVerifiedForUser($user);
|
||||
$loginService->reattemptLoginFor($user);
|
||||
$this->limiter->decrementAttempts($user, $request);
|
||||
|
||||
return redirect()->intended();
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ class LoginService
|
||||
/**
|
||||
* Clear the last login attempted session value.
|
||||
*/
|
||||
protected function clearLastLoginAttempted(): void
|
||||
public function clearLastLoginAttempted(): void
|
||||
{
|
||||
session()->remove(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY);
|
||||
}
|
||||
|
||||
62
app/Access/Mfa/MfaVerificationLimiter.php
Normal file
62
app/Access/Mfa/MfaVerificationLimiter.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Access\Mfa;
|
||||
|
||||
use BookStack\Exceptions\NotifyException;
|
||||
use BookStack\Users\Models\User;
|
||||
use Illuminate\Cache\RateLimiter;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
/**
|
||||
* A rate limit specifically for MFA verification.
|
||||
* Limits across both the attempted user (on a tight limit) and the
|
||||
* request IP (on a less strict limit).
|
||||
*/
|
||||
class MfaVerificationLimiter
|
||||
{
|
||||
protected int $maxUserAttemptsPerMinute = 5;
|
||||
protected int $maxIpAttemptsPerMinute = 60;
|
||||
|
||||
public function __construct(
|
||||
protected RateLimiter $rateLimiter
|
||||
) {
|
||||
}
|
||||
|
||||
public function throwException(): never
|
||||
{
|
||||
throw new NotifyException(
|
||||
trans('auth.mfa_throttle', ['seconds' => 60]),
|
||||
'/login',
|
||||
Response::HTTP_TOO_MANY_REQUESTS
|
||||
);
|
||||
}
|
||||
|
||||
public function incrementAttempts(User $user, Request $request): void
|
||||
{
|
||||
$this->rateLimiter->hit($this->getUserKey($user));
|
||||
$this->rateLimiter->hit($this->getRequestKey($request));
|
||||
}
|
||||
|
||||
public function decrementAttempts(User $user, Request $request): void
|
||||
{
|
||||
$this->rateLimiter->decrement($this->getUserKey($user));
|
||||
$this->rateLimiter->decrement($this->getRequestKey($request));
|
||||
}
|
||||
|
||||
public function hasHitLimit(User $user, Request $request): bool
|
||||
{
|
||||
return $this->rateLimiter->tooManyAttempts($this->getUserKey($user), $this->maxUserAttemptsPerMinute + 1)
|
||||
|| $this->rateLimiter->tooManyAttempts($this->getRequestKey($request), $this->maxIpAttemptsPerMinute + 1);
|
||||
}
|
||||
|
||||
protected function getUserKey(User $user): string
|
||||
{
|
||||
return "mfa-attempt::user::{$user->id}";
|
||||
}
|
||||
|
||||
protected function getRequestKey(Request $request): string
|
||||
{
|
||||
return "mfa-attempt::request::{$request->ip()}";
|
||||
}
|
||||
}
|
||||
@@ -120,8 +120,14 @@ class SearchRunner
|
||||
$filter = function (EloquentBuilder $query) use ($exact) {
|
||||
$inputTerm = str_replace('\\', '\\\\', $exact->value);
|
||||
$query->where('name', 'like', '%' . $inputTerm . '%')
|
||||
->orWhere('description', 'like', '%' . $inputTerm . '%')
|
||||
->orWhere('text', 'like', '%' . $inputTerm . '%');
|
||||
->orWhere(function (EloquentBuilder $query) use ($inputTerm) {
|
||||
$query->whereNotNull('description')
|
||||
->where('description', 'like', '%' . $inputTerm . '%');
|
||||
})
|
||||
->orWhere(function (EloquentBuilder $query) use ($inputTerm) {
|
||||
$query->whereNotNull('text')
|
||||
->where('text', 'like', '%' . $inputTerm . '%');
|
||||
});
|
||||
};
|
||||
|
||||
$exact->negated ? $entityQuery->whereNot($filter) : $entityQuery->where($filter);
|
||||
|
||||
@@ -195,6 +195,7 @@ class AttachmentController extends Controller
|
||||
$this->validate($request, [
|
||||
'order' => ['required', 'array'],
|
||||
]);
|
||||
|
||||
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
||||
|
||||
@@ -221,8 +222,6 @@ class AttachmentController extends Controller
|
||||
throw new NotFoundException(trans('errors.attachment_not_found'));
|
||||
}
|
||||
|
||||
$this->checkOwnablePermission(Permission::PageView, $page);
|
||||
|
||||
if ($attachment->external) {
|
||||
return redirect($attachment->path);
|
||||
}
|
||||
@@ -247,6 +246,13 @@ class AttachmentController extends Controller
|
||||
{
|
||||
/** @var Attachment $attachment */
|
||||
$attachment = Attachment::query()->findOrFail($attachmentId);
|
||||
|
||||
try {
|
||||
$this->pageQueries->findVisibleByIdOrFail($attachment->uploaded_to);
|
||||
} catch (NotFoundException $exception) {
|
||||
throw new NotFoundException(trans('errors.attachment_not_found'));
|
||||
}
|
||||
|
||||
$this->checkOwnablePermission(Permission::AttachmentDelete, $attachment);
|
||||
$this->attachmentService->deleteFile($attachment);
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ use BookStack\Exceptions\HttpFetchException;
|
||||
* Validate the host we're connecting to when making a server-side-request.
|
||||
* Will use the given hosts config if given during construction otherwise
|
||||
* will look to the app configured config.
|
||||
*
|
||||
* The config format is a space-seperated list of URL prefixes which should contain the
|
||||
* protocol and host. It can optionally define a path prefix as part of the URL.
|
||||
* Wildcards, via a '*', can be used within these elements to match anything but a '/'.
|
||||
*/
|
||||
class SsrUrlValidator
|
||||
{
|
||||
@@ -48,15 +52,34 @@ class SsrUrlValidator
|
||||
{
|
||||
$pattern = rtrim(trim($pattern), '/');
|
||||
$url = trim($url);
|
||||
$urlParts = parse_url($url);
|
||||
|
||||
if (empty($pattern) || empty($url)) {
|
||||
if (empty($pattern) || empty($url) || $urlParts === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$quoted = preg_quote($pattern, '/');
|
||||
$regexPattern = str_replace('\*', '.*', $quoted);
|
||||
// Prevent potential tricks using percent encoded slashes
|
||||
if (str_contains(strtolower($urlParts['host'] ?? ''), '%2f')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return preg_match('/^' . $regexPattern . '($|\/.*$|#.*$)/i', $url);
|
||||
// Disregard query and fragment
|
||||
$url = explode('?', $url, 2)[0];
|
||||
$url = explode('#', $url, 2)[0];
|
||||
|
||||
// Disregard userinfo if existing
|
||||
if (!empty($urlParts['user']) || !empty($urlParts['pass'])) {
|
||||
[$start, $postUserinfo] = explode('@', $url, 2);
|
||||
$preUserinfo = explode('//', $start, 2)[0];
|
||||
$url = ($preUserinfo ? $preUserinfo . '//' : '') . $postUserinfo;
|
||||
}
|
||||
|
||||
// Prepare pattern
|
||||
$quoted = preg_quote($pattern, '/');
|
||||
$regexPattern = str_replace('\*', '[^\/]*', $quoted);
|
||||
|
||||
// Check against our URL
|
||||
return preg_match('/^' . $regexPattern . '($|\/.*$)/i', $url);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
599
composer.lock
generated
599
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
||||
22e02ee72d21ff719c1073abbec8302f8e2096ba6d072e133051064ed24b45b1
|
||||
dae691f45e3714020abafdc1d6c10a50ae81fee9630e292237d75ca8b2070e6e
|
||||
@@ -8,6 +8,7 @@ return [
|
||||
|
||||
'failed' => 'These credentials do not match our records.',
|
||||
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
|
||||
'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.',
|
||||
|
||||
// Login & Register
|
||||
'sign_up' => 'Sign up',
|
||||
|
||||
33
public/dist/app.js
vendored
Normal file
33
public/dist/app.js
vendored
Normal file
File diff suppressed because one or more lines are too long
32
public/dist/code.js
vendored
Normal file
32
public/dist/code.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/dist/export-styles.css
vendored
Normal file
1
public/dist/export-styles.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
public/dist/legacy-modes.js
vendored
Normal file
3
public/dist/legacy-modes.js
vendored
Normal file
File diff suppressed because one or more lines are too long
28
public/dist/markdown.js
vendored
Normal file
28
public/dist/markdown.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/dist/styles.css
vendored
Normal file
1
public/dist/styles.css
vendored
Normal file
File diff suppressed because one or more lines are too long
32
public/dist/wysiwyg.js
vendored
Normal file
32
public/dist/wysiwyg.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -66,6 +66,27 @@ class MfaVerificationTest extends TestCase
|
||||
$html->assertElementExists('input[autocomplete="one-time-code"][name="code"]');
|
||||
}
|
||||
|
||||
public function test_totp_verification_is_rate_limited()
|
||||
{
|
||||
[$user, $secret, $loginResp] = $this->startTotpLogin();
|
||||
$loginService = $this->app->make(LoginService::class);
|
||||
|
||||
$resp = $this->get('/mfa/verify');
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$this->post('/mfa/totp/verify', [
|
||||
'code' => '123456',
|
||||
])->assertRedirect('/mfa/verify');
|
||||
$this->assertNotNull($loginService->getLastLoginAttemptUser());
|
||||
}
|
||||
|
||||
$resp = $this->post('/mfa/totp/verify', [
|
||||
'code' => '123456',
|
||||
]);
|
||||
$resp->assertRedirect('/login');
|
||||
$this->assertSessionError('Too many multi-factor verification attempts. Please try again in 60 seconds.');
|
||||
$this->assertNull($loginService->getLastLoginAttemptUser());
|
||||
}
|
||||
|
||||
public function test_backup_code_verification()
|
||||
{
|
||||
[$user, $codes, $loginResp] = $this->startBackupCodeLogin();
|
||||
@@ -147,6 +168,27 @@ class MfaVerificationTest extends TestCase
|
||||
$resp->assertSeeText('You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.');
|
||||
}
|
||||
|
||||
public function test_backup_code_verification_is_rate_limited()
|
||||
{
|
||||
[$user, $codes, $loginResp] = $this->startBackupCodeLogin(['abc12-def45', 'abc12-def46']);
|
||||
$loginService = $this->app->make(LoginService::class);
|
||||
|
||||
$resp = $this->get('/mfa/verify');
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$this->post('/mfa/backup_codes/verify', [
|
||||
'code' => '123456abcd',
|
||||
])->assertRedirect('/mfa/verify');
|
||||
$this->assertNotNull($loginService->getLastLoginAttemptUser());
|
||||
}
|
||||
|
||||
$resp = $this->post('/mfa/backup_codes/verify', [
|
||||
'code' => '123456abcd',
|
||||
]);
|
||||
$resp->assertRedirect('/login');
|
||||
$this->assertSessionError('Too many multi-factor verification attempts. Please try again in 60 seconds.');
|
||||
$this->assertNull($loginService->getLastLoginAttemptUser());
|
||||
}
|
||||
|
||||
public function test_backup_code_form_has_autofill_configured()
|
||||
{
|
||||
[$user, $codes, $loginResp] = $this->startBackupCodeLogin();
|
||||
|
||||
@@ -136,17 +136,21 @@ class EntitySearchTest extends TestCase
|
||||
$page->tags()->saveMany([new Tag(['name' => 'DonkCount', 'value' => '500'])]);
|
||||
$page->created_by = $this->users->admin()->id;
|
||||
$page->save();
|
||||
$otherPage = $this->entities->newPage(['name' => 'A different page in negation tests', 'html' => '<p>A different page in negation tests</p>']);
|
||||
|
||||
$editor = $this->users->editor();
|
||||
$this->actingAs($editor);
|
||||
|
||||
$exactSearch = $this->get('/search?term=' . urlencode('negation -"tortoise"'));
|
||||
$exactSearch->assertStatus(200)->assertDontSeeText($page->name);
|
||||
$exactSearch->assertSeeText($otherPage->name);
|
||||
|
||||
$tagSearchA = $this->get('/search?term=' . urlencode('negation [DonkCount=500]'));
|
||||
$tagSearchA->assertStatus(200)->assertSeeText($page->name);
|
||||
$tagSearchA->assertDontSeeText($otherPage->name);
|
||||
$tagSearchB = $this->get('/search?term=' . urlencode('negation -[DonkCount=500]'));
|
||||
$tagSearchB->assertStatus(200)->assertDontSeeText($page->name);
|
||||
$tagSearchB->assertSeeText($otherPage->name);
|
||||
|
||||
$filterSearchA = $this->get('/search?term=' . urlencode('negation -{created_by:me}'));
|
||||
$filterSearchA->assertStatus(200)->assertSeeText($page->name);
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use BookStack\Exceptions\HttpFetchException;
|
||||
use BookStack\Util\SsrUrlValidator;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SsrUrlValidatorTest extends TestCase
|
||||
{
|
||||
public function test_allowed()
|
||||
{
|
||||
$testMap = [
|
||||
// Single values
|
||||
['config' => '', 'url' => '', 'result' => false],
|
||||
['config' => '', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => ' ', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => '*', 'url' => '', 'result' => false],
|
||||
['config' => '*', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => 'https://*', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => 'http://*', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => 'https://*example.com', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => 'https://*ample.com', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => 'https://*.example.com', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => 'https://*.example.com', 'url' => 'https://test.example.com', 'result' => true],
|
||||
['config' => '*//example.com', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => '*//example.com', 'url' => 'http://example.com', 'result' => true],
|
||||
['config' => '*//example.co', 'url' => 'http://example.co.uk', 'result' => false],
|
||||
['config' => '*//example.co/bookstack', 'url' => 'https://example.co/bookstack/a/path', 'result' => true],
|
||||
['config' => '*//example.co*', 'url' => 'https://example.co.uk/bookstack/a/path', 'result' => true],
|
||||
['config' => 'https://example.com', 'url' => 'https://example.com/a/b/c?test=cat', 'result' => true],
|
||||
['config' => 'https://example.com', 'url' => 'https://example.co.uk', 'result' => false],
|
||||
|
||||
// Escapes
|
||||
['config' => 'https://(.*?).com', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => 'https://example.com', 'url' => 'https://example.co.uk#https://example.com', 'result' => false],
|
||||
|
||||
// Multi values
|
||||
['config' => '*//example.org *//example.com', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => '*//example.org *//example.com', 'url' => 'https://example.com/a/b/c?test=cat#hello', 'result' => true],
|
||||
['config' => '*.example.org *.example.com', 'url' => 'https://example.co.uk', 'result' => false],
|
||||
['config' => ' *.example.org *.example.com ', 'url' => 'https://example.co.uk', 'result' => false],
|
||||
['config' => '* *.example.com', 'url' => 'https://example.co.uk', 'result' => true],
|
||||
['config' => '*//example.org *//example.com *//example.co.uk', 'url' => 'https://example.co.uk', 'result' => true],
|
||||
['config' => '*//example.org *//example.com *//example.co.uk', 'url' => 'https://example.net', 'result' => false],
|
||||
];
|
||||
|
||||
foreach ($testMap as $test) {
|
||||
$result = (new SsrUrlValidator($test['config']))->allowed($test['url']);
|
||||
$this->assertEquals($test['result'], $result, "Failed asserting url '{$test['url']}' with config '{$test['config']}' results " . ($test['result'] ? 'true' : 'false'));
|
||||
}
|
||||
}
|
||||
|
||||
public function test_enssure_allowed()
|
||||
{
|
||||
$result = (new SsrUrlValidator('https://example.com'))->ensureAllowed('https://example.com');
|
||||
$this->assertNull($result);
|
||||
|
||||
$this->expectException(HttpFetchException::class);
|
||||
(new SsrUrlValidator('https://example.com'))->ensureAllowed('https://test.example.com');
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace Tests\Uploads;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Repos\PageRepo;
|
||||
use BookStack\Entities\Tools\TrashCan;
|
||||
use BookStack\Permissions\Permission;
|
||||
use BookStack\Uploads\Attachment;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -206,6 +207,21 @@ class AttachmentTest extends TestCase
|
||||
$this->files->deleteAllAttachmentFiles();
|
||||
}
|
||||
|
||||
public function test_attachment_deletion_requires_page_access()
|
||||
{
|
||||
$page = $this->entities->page();
|
||||
$attachment = Attachment::factory()->create(['uploaded_to' => $page->id]);
|
||||
$editor = $this->users->editor();
|
||||
|
||||
$this->permissions->disableEntityInheritedPermissions($page);
|
||||
$this->permissions->grantUserRolePermissions($editor, [Permission::AttachmentDeleteAll]);
|
||||
|
||||
$resp = $this->actingAs($editor)->delete($attachment->getUrl());
|
||||
$resp->assertNotFound();
|
||||
|
||||
$this->assertDatabaseHas('attachments', ['id' => $attachment->id]);
|
||||
}
|
||||
|
||||
public function test_attachment_access_without_permission_shows_404()
|
||||
{
|
||||
$admin = $this->users->admin();
|
||||
|
||||
142
tests/Util/SsrUrlValidatorTest.php
Normal file
142
tests/Util/SsrUrlValidatorTest.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Util;
|
||||
|
||||
use BookStack\Exceptions\HttpFetchException;
|
||||
use BookStack\Util\SsrUrlValidator;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SsrUrlValidatorTest extends TestCase
|
||||
{
|
||||
public function test_is_uses_app_config_by_default()
|
||||
{
|
||||
config()->set([
|
||||
'app.ssr_hosts' => 'https://donkey.example.com',
|
||||
]);
|
||||
|
||||
$validator = new SsrUrlValidator();
|
||||
|
||||
$this->assertTrue($validator->allowed('https://donkey.example.com'));
|
||||
$this->assertFalse($validator->allowed('https://monkey.example.com'));
|
||||
}
|
||||
|
||||
public function test_config_string_can_be_passed_in_constructor()
|
||||
{
|
||||
config()->set([
|
||||
'app.ssr_hosts' => 'https://donkey.example.com',
|
||||
]);
|
||||
|
||||
$validator = new SsrUrlValidator('https://monkey.example.com');
|
||||
|
||||
$this->assertFalse($validator->allowed('https://donkey.example.com'));
|
||||
$this->assertTrue($validator->allowed('https://monkey.example.com'));
|
||||
}
|
||||
|
||||
public function test_config_string_can_include_multiple_space_seperated_values()
|
||||
{
|
||||
$validator = new SsrUrlValidator('https://monkey.example.com https://cat.example.com');
|
||||
|
||||
$this->assertFalse($validator->allowed('https://donkey.example.com'));
|
||||
$this->assertTrue($validator->allowed('https://monkey.example.com'));
|
||||
$this->assertTrue($validator->allowed('https://cat.example.com'));
|
||||
}
|
||||
|
||||
public function test_ensure_allowed_throws_if_not_allowed()
|
||||
{
|
||||
$validator = new SsrUrlValidator('https://monkey.example.com');
|
||||
|
||||
$this->assertNull($validator->ensureAllowed('https://monkey.example.com'));
|
||||
|
||||
$this->assertThrows(function () use ($validator) {
|
||||
$validator->ensureAllowed('https://donkey.example.com');
|
||||
}, HttpFetchException::class, 'The URL does not match the configured allowed SSR hosts');
|
||||
}
|
||||
|
||||
public function test_basic_url_matching()
|
||||
{
|
||||
$tests = [
|
||||
// Single values
|
||||
['config' => '', 'url' => '', 'result' => false],
|
||||
['config' => '', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => ' ', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => '*', 'url' => '', 'result' => false],
|
||||
['config' => '*', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => 'https://*', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => 'http://*', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => 'https://*example.com', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => 'https://*ample.com', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => 'https://*.example.com', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => 'https://*.example.com', 'url' => 'https://test.example.com', 'result' => true],
|
||||
['config' => '*//example.com', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => '*//example.com', 'url' => 'http://example.com', 'result' => true],
|
||||
['config' => '*//example.co', 'url' => 'http://example.co.uk', 'result' => false],
|
||||
['config' => '*//example.co/bookstack', 'url' => 'https://example.co/bookstack/a/path', 'result' => true],
|
||||
['config' => '*//example.co*', 'url' => 'https://example.co.uk/bookstack/a/path', 'result' => true],
|
||||
['config' => 'https://example.com', 'url' => 'https://example.com/a/b/c?test=cat', 'result' => true],
|
||||
['config' => 'https://example.com', 'url' => 'https://example.co.uk', 'result' => false],
|
||||
|
||||
// Escapes
|
||||
['config' => 'https://(.*?).com', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => 'https://example.com', 'url' => 'https://example.co.uk#https://example.com', 'result' => false],
|
||||
|
||||
// Multi values
|
||||
['config' => '*//example.org *//example.com', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => '*//example.org *//example.com', 'url' => 'https://example.com/a/b/c?test=cat#hello', 'result' => true],
|
||||
['config' => '*.example.org *.example.com', 'url' => 'https://example.co.uk', 'result' => false],
|
||||
['config' => ' *.example.org *.example.com ', 'url' => 'https://example.co.uk', 'result' => false],
|
||||
['config' => '* *.example.com', 'url' => 'https://example.co.uk', 'result' => true],
|
||||
['config' => '*//example.org *//example.com *//example.co.uk', 'url' => 'https://example.co.uk', 'result' => true],
|
||||
['config' => '*//example.org *//example.com *//example.co.uk', 'url' => 'https://example.net', 'result' => false],
|
||||
|
||||
// Further tests
|
||||
['config' => 'https://monkey.example.com', 'url' => 'https://monkey.example.com/a/b', 'result' => true,],
|
||||
['config' => 'https://monkey.example.com', 'url' => 'https://monkey.example.com/a/b?a=b#ab', 'result' => true,],
|
||||
['config' => 'https://monkey.example.com', 'url' => 'https://monkey.example.com:8080/a', 'result' => false,],
|
||||
['config' => '*', 'url' => 'https://a.example.com', 'result' => true,],
|
||||
['config' => 'https://monkey.example.com', 'url' => 'http://monkey.example.com/a/b?a=b#ab', 'result' => false,],
|
||||
['config' => 'https://monkey.example.com', 'url' => 'https://beans.monkey.example.com/a/b?a=b#ab', 'result' => false,],
|
||||
['config' => 'https://*monkey.example.com', 'url' => 'https://amonkey.example.com/a/b?a=b#ab', 'result' => true,],
|
||||
['config' => 'https://*monkey.example.com', 'url' => 'https://donkey.example.com/a/b/monkey.example.com/b?a=b#ab', 'result' => false,],
|
||||
['config' => 'https://monkey.example.com', 'url' => 'https://example.com/monkey.example.com/b?a=monkey.example.com#monkey.example.com', 'result' => false,],
|
||||
['config' => 'https://*.example.com', 'url' => 'https://a.b.example.com/a/b', 'result' => true,],
|
||||
['config' => 'https://*.example.com', 'url' => 'https://a.b.example.a.com/a/b', 'result' => false,],
|
||||
['config' => 'https://*.example.com', 'url' => 'https://a.com/a/b?val=a.example.com', 'result' => false,],
|
||||
['config' => 'https://*.example.com', 'url' => 'https://a.com/a/b#example.com', 'result' => false,],
|
||||
['config' => 'https://a.*.example.com', 'url' => 'https://a.b.c.example.com/c/d', 'result' => true,],
|
||||
['config' => 'https://example.com/webhooks/', 'url' => 'https://example.com/webhooks/beans', 'result' => true,],
|
||||
['config' => 'https://example.com/webhooks/', 'url' => 'https://example.com/a/webhooks/', 'result' => false,],
|
||||
['config' => 'https://example.com:8080', 'url' => 'https://example.com/a/b', 'result' => false,],
|
||||
['config' => 'https://example.com:8080', 'url' => 'https://example.com:8080/a/b', 'result' => true,],
|
||||
['config' => 'https://example.com/*', 'url' => 'https://example.com:8080/a/b', 'result' => false,],
|
||||
];
|
||||
|
||||
foreach ($tests as $testCase) {
|
||||
$validator = new SsrUrlValidator($testCase['config']);
|
||||
$result = $validator->allowed($testCase['url']);
|
||||
$this->assertEquals($testCase['result'], $result, "Failed asserting expected result for config {$testCase['config']} and test value {$testCase['url']}");
|
||||
}
|
||||
}
|
||||
|
||||
public function test_wildcard_does_not_match_userinfo_data_but_still_allows_it()
|
||||
{
|
||||
$validator = new SsrUrlValidator('https://*monkey.example.com');
|
||||
$this->assertFalse($validator->allowed('https://monkey.example.com@a.example.com'));
|
||||
|
||||
$validator = new SsrUrlValidator('https://monkey.example.com*');
|
||||
$this->assertFalse($validator->allowed('https://monkey.example.com@a.example.com'));
|
||||
$this->assertFalse($validator->allowed('https://monkey.example.com:monkey.example.com@a.example.com'));
|
||||
|
||||
$validator = new SsrUrlValidator('https://monkey.example.com');
|
||||
$this->assertTrue($validator->allowed('https://a:b@monkey.example.com'));
|
||||
}
|
||||
|
||||
public function test_percent_encoded_slashes_in_host_are_rejected()
|
||||
{
|
||||
$validator = new SsrUrlValidator('*');
|
||||
|
||||
$this->assertFalse($validator->allowed('https://cat.example.com%2Fa/b'));
|
||||
$this->assertFalse($validator->allowed('https://cat.example.com%2fa/b'));
|
||||
$this->assertFalse($validator->allowed('https://cat%2f.example.com/a/b'));
|
||||
$this->assertFalse($validator->allowed('https://cat.exa%2Fmple.com'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user