Skip to content

Commit 96a7777

Browse files
committed
fix: corrected and improved permission checks
1 parent 2989146 commit 96a7777

File tree

17 files changed

+109
-16
lines changed

17 files changed

+109
-16
lines changed

phpmyfaq/admin/assets/src/api/tags.test.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('Tags API', () => {
1212
{ id: '1', name: 'Tag1' },
1313
{ id: '2', name: 'Tag2' },
1414
];
15-
global.fetch = vi.fn(() =>
15+
globalThis.fetch = vi.fn(() =>
1616
Promise.resolve({
1717
ok: true,
1818
json: () => Promise.resolve(mockResponse),
@@ -23,7 +23,7 @@ describe('Tags API', () => {
2323
const result = await fetchTags(searchString);
2424

2525
expect(result).toEqual(mockResponse);
26-
expect(global.fetch).toHaveBeenCalledWith('./api/content/tags?search=Tag', {
26+
expect(globalThis.fetch).toHaveBeenCalledWith('./api/content/tags?search=Tag', {
2727
method: 'GET',
2828
cache: 'no-cache',
2929
headers: {
@@ -36,7 +36,7 @@ describe('Tags API', () => {
3636

3737
it('should throw an error if fetch fails', async () => {
3838
const mockError = new Error('Fetch failed');
39-
global.fetch = vi.fn(() => Promise.reject(mockError));
39+
globalThis.fetch = vi.fn(() => Promise.reject(mockError));
4040

4141
const searchString = 'Tag';
4242

@@ -47,34 +47,37 @@ describe('Tags API', () => {
4747
describe('deleteTag', () => {
4848
it('should delete tag and return JSON response if successful', async () => {
4949
const mockResponse = { success: true, message: 'Tag deleted' };
50-
global.fetch = vi.fn(() =>
50+
globalThis.fetch = vi.fn(() =>
5151
Promise.resolve({
5252
ok: true,
5353
json: () => Promise.resolve(mockResponse),
5454
} as Response)
5555
);
5656

5757
const tagId = '1';
58-
await deleteTag(tagId);
58+
const csrfToken = 'test-csrf';
59+
await deleteTag(tagId, csrfToken);
5960

60-
expect(global.fetch).toHaveBeenCalledWith('./api/content/tags/1', {
61+
expect(globalThis.fetch).toHaveBeenCalledWith('./api/content/tags/1', {
6162
method: 'DELETE',
6263
cache: 'no-cache',
6364
headers: {
6465
'Content-Type': 'application/json',
6566
},
67+
body: JSON.stringify({ csrfToken: 'test-csrf' }),
6668
redirect: 'follow',
6769
referrerPolicy: 'no-referrer',
6870
});
6971
});
7072

7173
it('should throw an error if fetch fails', async () => {
7274
const mockError = new Error('Fetch failed');
73-
global.fetch = vi.fn(() => Promise.reject(mockError));
75+
globalThis.fetch = vi.fn(() => Promise.reject(mockError));
7476

7577
const tagId = '1';
78+
const csrfToken = 'test-csrf';
7679

77-
await expect(deleteTag(tagId)).rejects.toThrow(mockError);
80+
await expect(deleteTag(tagId, csrfToken)).rejects.toThrow(mockError);
7881
});
7982
});
8083
});

phpmyfaq/admin/assets/src/api/tags.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@ export const fetchTags = async (searchString: string): Promise<TagResponse[]> =>
3232
return await response.json();
3333
};
3434

35-
export const deleteTag = async (tagId: string): Promise<{ success?: string; error?: string }> => {
35+
export const deleteTag = async (tagId: string, csrfToken: string): Promise<{ success?: string; error?: string }> => {
3636
const response = await fetch(`./api/content/tags/${tagId}`, {
3737
method: 'DELETE',
3838
cache: 'no-cache',
3939
headers: {
4040
'Content-Type': 'application/json',
4141
},
42+
body: JSON.stringify({ csrfToken: csrfToken }),
4243
redirect: 'follow',
4344
referrerPolicy: 'no-referrer',
4445
});

phpmyfaq/admin/assets/src/content/tags.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@ export const handleTags = (): void => {
6262
element.addEventListener('click', async (event: Event) => {
6363
const target = event.target as HTMLElement;
6464
const tagId = target.getAttribute('data-pmf-id') as string;
65+
const csrfToken = (document.querySelector('input[name=pmf-csrf-token]') as HTMLInputElement).value;
6566

66-
const response = await deleteTag(tagId);
67+
const response = await deleteTag(tagId, csrfToken);
6768
if (response.success) {
6869
pushNotification(response.success);
6970
const row = document.getElementById(`pmf-row-tag-id-${tagId}`) as HTMLElement;

phpmyfaq/assets/templates/admin/configuration/instances.edit.twig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
</h1>
1010
</div>
1111
<form action="./instance/update" method="post" accept-charset="utf-8">
12+
<input type="hidden" name="pmf-csrf-token" value="{{ csrfTokenUpdateInstance }}"/>
1213
<input type="hidden" name="id" value="{{ instance.id }}"/>
1314
<div class="row mb-2">
1415
<label for="url" class="col-lg-2 col-form-label">{{ ad_instance_url }}:</label>

phpmyfaq/assets/templates/admin/user/group.twig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
<i class="bi bi-info-circle" aria-hidden="true"></i> {{ 'ad_group_details' | translate }}
4747
</h5>
4848
<form action="./group/update" method="post">
49+
<input type="hidden" name="pmf-csrf-token" value="{{ csrfTokenUpdateGroup }}">
4950
<input id="update_group_id" type="hidden" name="group_id" value="0">
5051
<div class="card-body">
5152
<div class="row mb-2">
@@ -91,6 +92,7 @@
9192

9293
<div class="col-lg-4" id="groupMemberships">
9394
<form id="group_membership" name="group_membership" method="post" action="./group/update/members">
95+
<input type="hidden" name="pmf-csrf-token" value="{{ csrfTokenUpdateGroupMembers }}">
9496
<input id="update_member_group_id" type="hidden" name="group_id" value="0">
9597
<div class="card shadow mb-4">
9698
<h5 class="card-header py-3">
@@ -170,6 +172,7 @@
170172

171173
<div id="groupRights" class="card shadow mb-4">
172174
<form id="rightsForm" action="./group/update/permissions" method="post">
175+
<input type="hidden" name="pmf-csrf-token" value="{{ csrfTokenUpdateGroupPermissions }}">
173176
<input id="rights_group_id" type="hidden" name="group_id" value="0">
174177
<h5 class="card-header py-3" id="user_rights_legend">
175178
<i aria-hidden="true" class="bi bi-lock"></i> {{ 'ad_group_rights' | translate }}

phpmyfaq/src/phpMyFAQ/Controller/AbstractController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ protected function isSecured(): void
161161
/**
162162
* @throws UnauthorizedHttpException
163163
*/
164-
protected function userIsAuthenticated(): void
164+
public function userIsAuthenticated(): void
165165
{
166166
if (!$this->currentUser->isLoggedIn()) {
167167
throw new UnauthorizedHttpException(challenge: 'User is not authenticated.');

phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/CategoryController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public function delete(Request $request): JsonResponse
9494
#[Route(path: 'admin/api/category/permissions', name: 'admin.api.category.permissions', methods: ['GET'])]
9595
public function permissions(Request $request): JsonResponse
9696
{
97-
$this->userIsAuthenticated();
97+
$this->userHasPermission(PermissionType::CATEGORY_EDIT);
9898

9999
$categoryPermission = $this->container->get(id: 'phpmyfaq.category.permission');
100100

@@ -119,7 +119,7 @@ public function permissions(Request $request): JsonResponse
119119
#[Route(path: 'admin/api/category/translations', name: 'admin.api.category.translations', methods: ['GET'])]
120120
public function translations(Request $request): JsonResponse
121121
{
122-
$this->userIsAuthenticated();
122+
$this->userHasPermission(PermissionType::CATEGORY_EDIT);
123123

124124
$category = new Category($this->configuration, [], false);
125125

phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/MarkdownController.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
2626
use League\CommonMark\MarkdownConverter;
2727
use phpMyFAQ\Controller\AbstractController;
28+
use phpMyFAQ\Enums\PermissionType;
2829
use phpMyFAQ\Filter;
2930
use Symfony\Component\HttpFoundation\JsonResponse;
3031
use Symfony\Component\HttpFoundation\Request;
@@ -39,6 +40,8 @@ final class MarkdownController extends AbstractController
3940
#[Route(path: 'admin/api/content/markdown')]
4041
public function renderMarkdown(Request $request): JsonResponse
4142
{
43+
$this->userHasPermission(PermissionType::FAQ_EDIT);
44+
4245
$data = json_decode($request->getContent());
4346

4447
$answer = Filter::filterVar($data->text, FILTER_SANITIZE_SPECIAL_CHARS);

phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/StatisticsController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ final class StatisticsController extends AbstractController
4040
#[Route(path: './admin/api/statistics/admin-log', methods: ['DELETE'])]
4141
public function deleteAdminLog(Request $request): JsonResponse
4242
{
43-
$this->userHasPermission(PermissionType::STATISTICS_VIEWLOGS);
43+
$this->userHasPermission(PermissionType::STATISTICS_ADMINLOG);
4444

4545
$data = json_decode($request->getContent(), false, 512, JSON_THROW_ON_ERROR);
4646

phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/TagController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ public function delete(Request $request): JsonResponse
116116
{
117117
$this->userHasPermission(PermissionType::FAQ_EDIT);
118118

119+
$data = json_decode($request->getContent());
120+
121+
if (!Token::getInstance($this->container->get(id: 'session'))->verifyToken('tags', $data->csrfToken ?? '')) {
122+
return $this->json(['error' => Translation::get(key: 'msgNoPermission')], Response::HTTP_UNAUTHORIZED);
123+
}
124+
119125
$tagId = (int) Filter::filterVar($request->attributes->get('tagId'), FILTER_VALIDATE_INT);
120126

121127
if ($this->container->get(id: 'phpmyfaq.tags')->delete($tagId)) {

0 commit comments

Comments
 (0)