fix(admin): guard GET /api/users/{id} with @RequirePermission(ADMIN_USER)

Fixes IDOR: the endpoint was publicly accessible to any authenticated user.
Now requires ADMIN_USER permission, matching all other user management endpoints.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-30 01:09:40 +02:00
committed by marcel
parent c61b08d6de
commit 9cacc6079e
22 changed files with 844 additions and 346 deletions

View File

@@ -29,64 +29,65 @@ let {
</a>
</div>
<table class="min-w-full divide-y divide-line">
<thead class="bg-muted">
<tr>
<th class="px-6 py-3 text-left text-xs font-bold tracking-wider text-ink-2 uppercase"
>{m.admin_col_login()}</th
>
<th class="px-6 py-3 text-left text-xs font-bold tracking-wider text-ink-2 uppercase"
>{m.admin_col_full_name()}</th
>
<th class="px-6 py-3 text-left text-xs font-bold tracking-wider text-ink-2 uppercase"
>{m.admin_col_groups()}</th
>
<th class="px-6 py-3 text-right text-xs font-bold tracking-wider text-ink-2 uppercase"
>{m.admin_col_actions()}</th
>
</tr>
</thead>
<tbody class="divide-y divide-line bg-surface">
{#each users as user (user.id)}
<tr class="group/row hover:bg-muted">
<td class="px-6 py-4 text-sm font-medium whitespace-nowrap text-ink">
{user.username}
</td>
<td class="px-6 py-4 text-sm whitespace-nowrap text-ink-2">
{#if user.firstName || user.lastName}
{user.firstName ?? ''} {user.lastName ?? ''}
{:else}
<span class="text-ink-3 italic"></span>
{/if}
</td>
<td class="px-6 py-4 text-sm text-ink-2">
<div class="flex flex-wrap gap-1">
{#if user.groups && user.groups.length > 0}
{#each user.groups as group (group.id)}
<span
class="rounded-full border border-blue-100 bg-blue-50 px-2 py-0.5 text-[10px] font-bold text-blue-700 uppercase"
>
{group.name}
</span>
{/each}
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-line">
<thead class="bg-muted">
<tr>
<th class="px-6 py-3 text-left text-xs font-bold tracking-wider text-ink-2 uppercase"
>{m.admin_col_login()}</th
>
<th class="px-6 py-3 text-left text-xs font-bold tracking-wider text-ink-2 uppercase"
>{m.admin_col_full_name()}</th
>
<th class="px-6 py-3 text-left text-xs font-bold tracking-wider text-ink-2 uppercase"
>{m.admin_col_groups()}</th
>
<th class="px-6 py-3 text-right text-xs font-bold tracking-wider text-ink-2 uppercase"
>{m.admin_col_actions()}</th
>
</tr>
</thead>
<tbody class="divide-y divide-line bg-surface">
{#each users as user (user.id)}
<tr class="group/row hover:bg-muted">
<td class="px-6 py-4 text-sm font-medium whitespace-nowrap text-ink">
{user.username}
</td>
<td class="px-6 py-4 text-sm whitespace-nowrap text-ink-2">
{#if user.firstName || user.lastName}
{user.firstName ?? ''} {user.lastName ?? ''}
{:else}
<span class="text-xs text-ink-3 italic">{m.admin_no_groups()}</span>
<span class="text-ink-3 italic"></span>
{/if}
</div>
</td>
<td class="px-6 py-4 text-right whitespace-nowrap">
<div class="flex items-center justify-end gap-4">
<a
href="/admin/users/{user.id}"
class="text-sm font-bold tracking-wide text-primary uppercase hover:text-ink-2"
>
{m.btn_edit()}
</a>
</td>
<td class="px-6 py-4 text-sm text-ink-2">
<div class="flex flex-wrap gap-1">
{#if user.groups && user.groups.length > 0}
{#each user.groups as group (group.id)}
<span
class="rounded-full border border-blue-100 bg-blue-50 px-2 py-0.5 text-[10px] font-bold text-blue-700 uppercase"
>
{group.name}
</span>
{/each}
{:else}
<span class="text-xs text-ink-3 italic">{m.admin_no_groups()}</span>
{/if}
</div>
</td>
<td class="px-6 py-4 text-right whitespace-nowrap">
<div class="flex items-center justify-end gap-4">
<a
href="/admin/users/{user.id}"
class="text-sm font-bold tracking-wide text-primary uppercase hover:text-ink-2"
>
{m.btn_edit()}
</a>
<form
method="POST"
action="?/deleteUser"
use:enhance={({ cancel }) => {
<form
method="POST"
action="?/deleteUser"
use:enhance={({ cancel }) => {
if (!confirm(m.admin_user_delete_confirm({ username: user.username }))) {
cancel();
}
@@ -94,27 +95,28 @@ let {
await update();
};
}}
class="flex items-center"
>
<input type="hidden" name="id" value={user.id} />
<button
class="p-1 text-ink-3 transition-colors hover:text-red-600"
title={m.admin_btn_delete_user_title()}
class="flex items-center"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</form>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
<input type="hidden" name="id" value={user.id} />
<button
class="p-1 text-ink-3 transition-colors hover:text-red-600"
title={m.admin_btn_delete_user_title()}
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</form>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>