Skip to content

Commit ce01a59

Browse files
committed
Validate AuthState before processing it.
We accept AuthState via untrusted URL parameters. As a defense in depth measure, validate that it conforms to our expected format before we output it again in HTML, feed it to a database, use the URL or do any further processing with it. Closes: #1706
1 parent 447c605 commit ce01a59

3 files changed

Lines changed: 58 additions & 2 deletions

File tree

modules/core/src/Controller/Login.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ public function loginuserpass(Request $request): Template
110110
throw new Error\BadRequest('Missing AuthState parameter.');
111111
}
112112
$authStateId = $request->query->get('AuthState');
113+
$this->authState::validateStateId($authStateId);
113114

114115
$state = $this->authState::loadState($authStateId, UserPassBase::STAGEID);
115116

@@ -137,6 +138,7 @@ private function handleLogin(Request $request, $source, array $state): Template
137138
{
138139
Assert::isInstanceOfAny($source, [UserPassBase::class, UserPassOrgBase::class]);
139140
$authStateId = $request->query->get('AuthState');
141+
$this->authState::validateStateId($authStateId);
140142

141143
$organizations = $organization = null;
142144
if ($source instanceof UserPassOrgBase) {
@@ -325,6 +327,7 @@ public function loginuserpassorg(Request $request): Template
325327
throw new Error\BadRequest('Missing AuthState parameter.');
326328
}
327329
$authStateId = $request->query->get('AuthState');
330+
$this->authState::validateStateId($authStateId);
328331

329332
$state = $this->authState::loadState($authStateId, UserPassOrgBase::STAGEID);
330333

src/SimpleSAML/Auth/State.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44

55
namespace SimpleSAML\Auth;
66

7+
use Exception;
78
use SimpleSAML\Assert\Assert;
89
use SimpleSAML\Configuration;
910
use SimpleSAML\Error;
1011
use SimpleSAML\Logger;
1112
use SimpleSAML\Session;
1213
use SimpleSAML\Utils;
1314

15+
use function filter_var;
16+
use function preg_match;
17+
1418
/**
1519
* This is a helper class for saving and loading state information.
1620
*
@@ -171,6 +175,22 @@ public static function getStateId(array &$state, bool $rawId = false): string
171175
return $id . ':' . $state[self::RESTART];
172176
}
173177

178+
/**
179+
* Perform syntactic validation of an incoming state ID.
180+
*
181+
* @throws \Exception If the syntax of the supplied state ID is unexpected.
182+
*/
183+
public static function validateStateId(string $stateId): void
184+
{
185+
$parts = explode(':', $stateId, 2);
186+
187+
if (!preg_match('/^_[0-9a-f]+$/', $parts[0])) {
188+
throw new Exception("Invalid AuthState ID syntax: " . $parts[0]);
189+
}
190+
if (!empty($parts[1]) && filter_var($parts[1], FILTER_VALIDATE_URL) === false) {
191+
throw new Exception("Invalid AuthState return URL syntax: " . $parts[1]);
192+
}
193+
}
174194

175195
/**
176196
* Retrieve state timeout.
@@ -303,7 +323,7 @@ public static function loadState(string $id, string $stage, bool $allowMissing =
303323
Logger::warning($msg);
304324

305325
if ($sid['url'] === null) {
306-
throw new \Exception($msg);
326+
throw new Exception($msg);
307327
}
308328

309329
$httpUtils->redirectUntrustedURL($sid['url']);
@@ -368,7 +388,7 @@ public static function throwException(array $state, Error\Exception $exception):
368388
*/
369389
throw $exception;
370390
}
371-
throw new \Exception(); // This should never happen
391+
throw new Exception(); // This should never happen
372392
}
373393

374394

tests/src/SimpleSAML/Auth/StateTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace SimpleSAML\Test\Auth;
66

7+
use Exception;
78
use PHPUnit\Framework\TestCase;
89
use SimpleSAML\Auth;
910

@@ -83,4 +84,36 @@ public function testGetPersistentAuthData(): void
8384
'Some error occurred with additional, persistent parameters, and no mandatory ones'
8485
);
8586
}
87+
88+
89+
public function testValidateStateIdSimple(): void
90+
{
91+
Auth\State::validateStateId('_aaabb');
92+
Auth\State::validateStateId('_aad12abb');
93+
Auth\State::validateStateId('_6938b6453edfcb8c68055555d22c346857d6cd55fa');
94+
$this->assertTrue(true);
95+
}
96+
97+
98+
public function testValidateStateIdWithReturnURL(): void
99+
{
100+
Auth\State::validateStateId('_aaabb:https://loeki.tv/wp-login.php?example=testsomething&urn=urn:example:org');
101+
$this->assertTrue(true);
102+
}
103+
104+
105+
public function testValidateStateIdSimpleInvalid(): void
106+
{
107+
$this->expectException(Exception::class);
108+
$this->expectExceptionMessage('Invalid AuthState ID syntax');
109+
Auth\State::validateStateId('aaabb');
110+
}
111+
112+
113+
public function testValidateStateIdWithReturnURLWhitespaceInvalid(): void
114+
{
115+
$this->expectException(Exception::class);
116+
$this->expectExceptionMessage('Invalid AuthState return URL syntax');
117+
Auth\State::validateStateId("_aaabb:http://loeki.tv/\nnot-allowed");
118+
}
86119
}

0 commit comments

Comments
 (0)