Skip to content

Commit 4bc8355

Browse files
committed
Fix for CLN-006 in ServerController
Resolves CWE-601 - URL Redirection to Untrusted Site ('Open Redirect')
1 parent 03d58e5 commit 4bc8355

File tree

2 files changed

+101
-31
lines changed

2 files changed

+101
-31
lines changed

solid/lib/Controller/ServerController.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,26 @@ public function authorize() {
220220
return $result; // ->addHeader('Access-Control-Allow-Origin', '*');
221221
}
222222

223+
if (isset($getVars['redirect_uri'])) {
224+
$redirectUri = $getVars['redirect_uri'];
225+
if (! isset($clientRegistration['redirect_uris']) || ! is_array($clientRegistration['redirect_uris'])) {
226+
return new JSONResponse('Invalid client registration, no redirect URIs found', Http::STATUS_BAD_REQUEST);
227+
}
228+
229+
$redirectUris = $clientRegistration['redirect_uris'];
230+
231+
$validRedirectUris = array_filter($redirectUris, function ($uri) use ($redirectUri) {
232+
// @CHECKME: Does either URI need to be normalized when it is a URL?
233+
// For instance, anchors `#` or query parameters `?`
234+
return $uri === $redirectUri;
235+
});
236+
237+
if (count($validRedirectUris) === 0) {
238+
return new JSONResponse('Provided redirect URI does not match any registered URIs', Http::STATUS_BAD_REQUEST);
239+
}
240+
}
241+
242+
// @CHECKME: Can more than one redirect_uri could be provided for custom schemes?
223243
$parsedOrigin = parse_url($clientRegistration['redirect_uris'][0]);
224244
if (
225245
$parsedOrigin['scheme'] != "https" &&

solid/tests/Unit/Controller/ServerControllerTest.php

Lines changed: 81 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -171,19 +171,14 @@ public function testAuthorizeWithoutApprovedClient()
171171
}
172172

173173
/**
174-
* @testdox ServerController should return a 302 redirect when asked to authorize client that has been approved
174+
* @testdox ServerController should return a 400 when asked to authorize a client that sends an incorrect redirect URI
175175
*
176176
* @covers ::authorize
177177
*/
178-
public function testAuthorizeWithApprovedClient()
178+
public function testAuthorizeWithInvalidRedirectUri()
179179
{
180180
$_GET['client_id'] = self::MOCK_CLIENT_ID;
181-
$_GET['nonce'] = 'mock-nonce';
182-
// JWT with empty payload, HS256 encoded, created with `private.key` from fixtures
183-
$_GET['request'] = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.8VKCTiBegJPuPIZlp0wbV0Sbdn5BS6TE5DCx6oYNc5o';
184-
$_GET['response_type'] = 'mock-response-type';
185-
186-
$_SERVER['REQUEST_URI'] = 'https://mock.server';
181+
$_GET['redirect_uri'] = 'https://some.other.client/redirect';
187182

188183
$clientData = json_encode(['client_name' => 'Mock Client', 'redirect_uris' => ['https://mock.client/redirect']]);
189184

@@ -200,15 +195,15 @@ public function testAuthorizeWithApprovedClient()
200195
$response = $controller->authorize();
201196

202197
$expected = [
203-
'data' => 'ok',
198+
'data' => 'Provided redirect URI does not match any registered URIs',
204199
'headers' => [
205200
'Cache-Control' => 'no-cache, no-store, must-revalidate',
206201
'Content-Security-Policy' => "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'",
207202
'Content-Type' => 'application/json; charset=utf-8',
208203
'Feature-Policy' => "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'",
209204
'X-Robots-Tag' => 'noindex, nofollow',
210205
],
211-
'status' => Http::STATUS_FOUND,
206+
'status' => Http::STATUS_BAD_REQUEST,
212207
];
213208

214209
$actual = [
@@ -217,32 +212,87 @@ public function testAuthorizeWithApprovedClient()
217212
'status' => $response->getStatus(),
218213
];
219214

220-
$location = $actual['headers']['Location'];
221-
222215
// Not comparing time-sensitive data
223-
unset($actual['headers']['X-Request-Id'], $actual['headers']['Location']);
216+
unset($actual['headers']['X-Request-Id']);
224217

225218
$this->assertEquals($expected, $actual);
226-
227-
// @TODO: Move $location assert to a separate test
228-
$url = parse_url($location);
229-
230-
parse_str($url['fragment'], $url['fragment']);
231-
232-
unset($url['fragment']['access_token'], $url['fragment']['id_token']);
233-
234-
$this->assertEquals([
235-
'scheme' => 'https',
236-
'host' => 'mock.client',
237-
'path' => '/redirect',
238-
'fragment' => [
239-
'token_type' => 'Bearer',
240-
'expires_in' => '3600',
241-
],
242-
], $url);
243219
}
244220

245-
/**
221+
/**
222+
* @testdox ServerController should return a 302 redirect when asked to authorize client that has been approved
223+
*
224+
* @covers ::authorize
225+
*/
226+
public function testAuthorize()
227+
{
228+
$_GET['client_id'] = self::MOCK_CLIENT_ID;
229+
$_GET['nonce'] = 'mock-nonce';
230+
// JWT with empty payload, HS256 encoded, created with `private.key` from fixtures
231+
$_GET['request'] = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.8VKCTiBegJPuPIZlp0wbV0Sbdn5BS6TE5DCx6oYNc5o';
232+
$_GET['response_type'] = 'mock-response-type';
233+
$_GET['redirect_uri'] = 'https://mock.client/redirect';
234+
235+
$_SERVER['REQUEST_URI'] = 'https://mock.server';
236+
$_SERVER['REQUEST_URI'];
237+
238+
$clientData = json_encode(['client_name' => 'Mock Client', 'redirect_uris' => ['https://mock.client/redirect']]);
239+
240+
$parameters = $this->createMockConstructorParameters($clientData);
241+
242+
$this->mockConfig->method('getUserValue')
243+
->with(self::MOCK_USER_ID, Application::APP_ID, 'allowedClients', '[]')
244+
->willReturn(json_encode([self::MOCK_CLIENT_ID]));
245+
246+
$this->mockUserManager->method('userExists')->willReturn(true);
247+
248+
$controller = new ServerController(...$parameters);
249+
250+
$response = $controller->authorize();
251+
252+
$expected = [
253+
'data' => 'ok',
254+
'headers' => [
255+
'Cache-Control' => 'no-cache, no-store, must-revalidate',
256+
'Content-Security-Policy' => "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'",
257+
'Content-Type' => 'application/json; charset=utf-8',
258+
'Feature-Policy' => "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'",
259+
'X-Robots-Tag' => 'noindex, nofollow',
260+
],
261+
'status' => Http::STATUS_FOUND,
262+
];
263+
264+
$actual = [
265+
'data' => $response->getData(),
266+
'headers' => $response->getHeaders(),
267+
'status' => $response->getStatus(),
268+
];
269+
270+
$location = $actual['headers']['Location'] ?? '';
271+
272+
// Not comparing time-sensitive data
273+
unset($actual['headers']['X-Request-Id'], $actual['headers']['Location']);
274+
275+
$this->assertEquals($expected, $actual);
276+
277+
// @TODO: Move $location assert to a separate test
278+
$url = parse_url($location);
279+
280+
parse_str($url['fragment'], $url['fragment']);
281+
282+
unset($url['fragment']['access_token'], $url['fragment']['id_token']);
283+
284+
$this->assertEquals([
285+
'scheme' => 'https',
286+
'host' => 'mock.client',
287+
'path' => '/redirect',
288+
'fragment' => [
289+
'token_type' => 'Bearer',
290+
'expires_in' => '3600',
291+
],
292+
], $url);
293+
}
294+
295+
/**
246296
* @testdox ServerController should return a 400 when asked to register without valid client data
247297
*
248298
* @covers ::register

0 commit comments

Comments
 (0)