Skip to content

Commit f9dbbe5

Browse files
committed
OIDC: Updated picture fetch implementation during review
Review of #5429
1 parent 05f7f4c commit f9dbbe5

File tree

4 files changed

+38
-28
lines changed

4 files changed

+38
-28
lines changed

app/Access/Oidc/OidcService.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,17 +222,17 @@ protected function processAccessTokenCallback(OidcAccessToken $accessToken, Oidc
222222
throw new OidcException($exception->getMessage());
223223
}
224224

225+
if ($this->config()['fetch_avatar'] && $user->wasRecentlyCreated && $userDetails->picture) {
226+
$this->userAvatars->assignToUserFromUrl($user, $userDetails->picture);
227+
}
228+
225229
if ($this->shouldSyncGroups()) {
226230
$detachExisting = $this->config()['remove_from_groups'];
227231
$this->groupService->syncUserWithFoundGroups($user, $userDetails->groups ?? [], $detachExisting);
228232
}
229233

230234
$this->loginService->login($user, 'oidc');
231235

232-
if ($this->config()['fetch_avatars'] && $userDetails->picture) {
233-
$this->userAvatars->assignToUserFromUrl($user, $userDetails->picture, $accessToken->getToken());
234-
}
235-
236236
return $user;
237237
}
238238

app/Access/Oidc/OidcUserDetails.php

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,16 @@ public function populate(
4141
$this->email = $claims->getClaim('email') ?? $this->email;
4242
$this->name = static::getUserDisplayName($displayNameClaims, $claims) ?? $this->name;
4343
$this->groups = static::getUserGroups($groupsClaim, $claims) ?? $this->groups;
44-
$this->picture = $claims->getClaim('picture') ?: $this->picture;
44+
$this->picture = static::getPicture($claims) ?: $this->picture;
4545
}
4646

47-
protected static function getUserDisplayName(string $displayNameClaims, ProvidesClaims $token): string
47+
protected static function getUserDisplayName(string $displayNameClaims, ProvidesClaims $claims): string
4848
{
4949
$displayNameClaimParts = explode('|', $displayNameClaims);
5050

5151
$displayName = [];
5252
foreach ($displayNameClaimParts as $claim) {
53-
$component = $token->getClaim(trim($claim)) ?? '';
53+
$component = $claims->getClaim(trim($claim)) ?? '';
5454
if ($component !== '') {
5555
$displayName[] = $component;
5656
}
@@ -59,13 +59,13 @@ protected static function getUserDisplayName(string $displayNameClaims, Provides
5959
return implode(' ', $displayName);
6060
}
6161

62-
protected static function getUserGroups(string $groupsClaim, ProvidesClaims $token): ?array
62+
protected static function getUserGroups(string $groupsClaim, ProvidesClaims $claims): ?array
6363
{
6464
if (empty($groupsClaim)) {
6565
return null;
6666
}
6767

68-
$groupsList = Arr::get($token->getAllClaims(), $groupsClaim);
68+
$groupsList = Arr::get($claims->getAllClaims(), $groupsClaim);
6969
if (!is_array($groupsList)) {
7070
return null;
7171
}
@@ -74,4 +74,14 @@ protected static function getUserGroups(string $groupsClaim, ProvidesClaims $tok
7474
return is_string($val);
7575
}));
7676
}
77+
78+
protected static function getPicture(ProvidesClaims $claims): ?string
79+
{
80+
$picture = $claims->getClaim('picture');
81+
if (is_string($picture) && str_starts_with($picture, 'http')) {
82+
return $picture;
83+
}
84+
85+
return null;
86+
}
7787
}

app/Config/oidc.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,16 @@
4747
// Multiple values can be provided comma seperated.
4848
'additional_scopes' => env('OIDC_ADDITIONAL_SCOPES', null),
4949

50+
// Enable fetching of the user's avatar from the 'picture' claim on initial login.
51+
// This can be a security risk due to performing server-side fetching of data from external URLs.
52+
// Only enable if you trust the OIDC auth provider to provide safe URLs for user images.
53+
'fetch_avatar' => env('OIDC_FETCH_AVATAR', false),
54+
5055
// Group sync options
5156
// Enable syncing, upon login, of OIDC groups to BookStack roles
5257
'user_to_groups' => env('OIDC_USER_TO_GROUPS', false),
5358
// Attribute, within a OIDC ID token, to find group names within
5459
'groups_claim' => env('OIDC_GROUPS_CLAIM', 'groups'),
5560
// When syncing groups, remove any groups that no longer match. Otherwise, sync only adds new groups.
5661
'remove_from_groups' => env('OIDC_REMOVE_FROM_GROUPS', false),
57-
58-
// When enabled, BookStack will fetch the user’s avatar from the 'picture' claim (SSRF risk if URLs are untrusted).
59-
'fetch_avatars' => env('OIDC_FETCH_AVATARS', false),
6062
];

app/Uploads/UserAvatars.php

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use BookStack\Exceptions\HttpFetchException;
66
use BookStack\Http\HttpRequestService;
77
use BookStack\Users\Models\User;
8+
use BookStack\Util\WebSafeMimeSniffer;
89
use Exception;
910
use GuzzleHttp\Psr7\Request;
1011
use Illuminate\Support\Facades\Log;
@@ -56,17 +57,19 @@ public function assignToUserFromExistingData(User $user, string $imageData, stri
5657
/**
5758
* Assign a new avatar image to the given user by fetching from a remote URL.
5859
*/
59-
public function assignToUserFromUrl(User $user, string $avatarUrl, ?string $accessToken = null): void
60+
public function assignToUserFromUrl(User $user, string $avatarUrl): void
6061
{
61-
// Quickly skip invalid or non-HTTP URLs
62-
if (!$avatarUrl || !str_starts_with($avatarUrl, 'http')) {
63-
return;
64-
}
65-
6662
try {
6763
$this->destroyAllForUser($user);
68-
$imageData = $this->getAvatarImageData($avatarUrl, $accessToken);
69-
$avatar = $this->createAvatarImageFromData($user, $imageData, 'png');
64+
$imageData = $this->getAvatarImageData($avatarUrl);
65+
66+
$mime = (new WebSafeMimeSniffer())->sniff($imageData);
67+
[$format, $type] = explode('/', $mime, 2);
68+
if ($format !== 'image' || ImageService::isExtensionSupported($type)) {
69+
return;
70+
}
71+
72+
$avatar = $this->createAvatarImageFromData($user, $imageData, $type);
7073
$user->avatar()->associate($avatar);
7174
$user->save();
7275
} catch (Exception $e) {
@@ -130,20 +133,15 @@ protected function createAvatarImageFromData(User $user, string $imageData, stri
130133
}
131134

132135
/**
133-
* Gets an image from a URL (public or private) and returns it as a string of image data.
136+
* Get an image from a URL and return it as a string of image data.
134137
*
135138
* @throws HttpFetchException
136139
*/
137-
protected function getAvatarImageData(string $url, ?string $accessToken = null): string
140+
protected function getAvatarImageData(string $url): string
138141
{
139142
try {
140-
$headers = [];
141-
if (!empty($accessToken)) {
142-
$headers['Authorization'] = 'Bearer ' . $accessToken;
143-
}
144-
145143
$client = $this->http->buildClient(5);
146-
$response = $client->sendRequest(new Request('GET', $url, $headers));
144+
$response = $client->sendRequest(new Request('GET', $url));
147145

148146
if ($response->getStatusCode() !== 200) {
149147
throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url]));

0 commit comments

Comments
 (0)