-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathUserOverwriteSecurityTest.php
More file actions
134 lines (115 loc) · 5.14 KB
/
UserOverwriteSecurityTest.php
File metadata and controls
134 lines (115 loc) · 5.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
<?php
use Grav\Common\Flex\Types\Users\UserObject;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
/**
* Test double that lets us drive UserObject::save() without a full Flex
* directory/blueprint/validator stack. We override the handful of accessors
* the uniqueness-guard block reads.
*/
class StubUserObject extends UserObject
{
public static ?FlexStorageInterface $stubStorage = null;
public static ?FlexDirectory $stubDirectory = null;
public string $stubKey = '';
public string $stubStorageKey = '';
// Match FlexObjectInterface constructor signature, but skip all parent work.
public function __construct(array $elements = [], $key = '', ?FlexDirectory $directory = null, bool $validate = false)
{
// Deliberately empty — we don't want blueprint/validator/container boot.
}
public function getKey(): string
{
return $this->stubKey;
}
public function getStorageKey(): string
{
return $this->stubStorageKey;
}
public function setStorageKey($key = null)
{
$this->stubStorageKey = (string)($key ?? '');
return $this;
}
public function getFlexDirectory(?string $type = null): FlexDirectory
{
return self::$stubDirectory;
}
}
/**
* Class UserOverwriteSecurityTest
*
* Covers: GHSA-rr73-568v-28f8 (privilege de-escalation / admin account disruption
* by creating a user with an existing username and overwriting the target).
*
* Verifies the guard in UserObject::save() that refuses to create a new user
* whose chosen username is already taken. The test also pins the two subtle
* properties of the check:
* - `@@`-prefixed transient storage keys (Flex's in-memory marker for
* unsaved objects) are treated as "new user" and trigger the check —
* `strpos($key, '@@')` would return 0 here and be falsy, bypassing the check.
* - The uniqueness check runs for any FlexStorageInterface, not just FileStorage.
*
* Naming convention: test{Method}_{GHSA_ID}_{description}
*/
class UserOverwriteSecurityTest extends \PHPUnit\Framework\TestCase
{
private function makeStubbedUser(string $storageKey, string $targetKey, bool $targetExists): StubUserObject
{
$storage = $this->createMock(FlexStorageInterface::class);
$storage->method('hasKey')->willReturnCallback(
static fn(string $k): bool => $k === $targetKey && $targetExists
);
$directory = $this->createMock(FlexDirectory::class);
$directory->method('getStorage')->willReturn($storage);
StubUserObject::$stubStorage = $storage;
StubUserObject::$stubDirectory = $directory;
$user = new StubUserObject();
$user->stubStorageKey = $storageKey;
$user->stubKey = $targetKey;
return $user;
}
// =========================================================================
// GHSA-rr73-568v-28f8: create-new path must refuse a taken username
// =========================================================================
public function testSave_GHSArr73_BlocksNewUserWithExistingUsername(): void
{
$user = $this->makeStubbedUser(storageKey: '', targetKey: 'root0', targetExists: true);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('User account with this username already exists');
$user->save();
}
public function testSave_GHSArr73_BlocksWhenTransientKeyStartsWithAtMarker(): void
{
// Flex's in-memory marker for unsaved objects is `@@<hash>`. `strpos($key, '@@')`
// returns 0 here, which is falsy — the old check would have skipped the guard
// and let the overwrite through. Pin the str_contains() fix.
$user = $this->makeStubbedUser(storageKey: '@@abc123', targetKey: 'root0', targetExists: true);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('User account with this username already exists');
$user->save();
}
public function testSave_GHSArr73_AllowsNewUserWhenUsernameIsFree(): void
{
// When the username is NOT taken, the uniqueness check should pass and
// save() must be allowed to proceed past the guard. We can't run the
// rest of save() without a full Flex setup, so we assert that the
// specific RuntimeException is NOT thrown before control leaves our
// instrumented code — any other exception downstream is acceptable.
$user = $this->makeStubbedUser(storageKey: '', targetKey: 'brand-new-user', targetExists: false);
try {
$user->save();
$this->addToAssertionCount(1);
} catch (RuntimeException $e) {
$this->assertStringNotContainsString(
'User account with this username already exists',
$e->getMessage(),
'Uniqueness guard must not fire when the target username is free'
);
} catch (\Throwable) {
// Downstream failures (missing blueprint, storage, events) are expected
// and do not indicate a guard regression.
$this->addToAssertionCount(1);
}
}
}