Skip to content

Commit 32ec8dd

Browse files
committed
fix: protect administrator comments
1 parent d5d79d4 commit 32ec8dd

4 files changed

Lines changed: 8 additions & 2 deletions

File tree

‎admin/tests/comments.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ it('lets an authenticated administrator edit and delete administrator comments w
7676
body: JSON.stringify(body),
7777
}), env);
7878
const { id } = await (await adminCall('/admin', 'POST', { page, body: '관리자 댓글' })).json() as { id: string };
79+
expect((await call(`/${id}`, 'DELETE', { password: '1234', version: 1 })).status).toBe(403);
7980
expect((await adminCall(`/${id}`, 'PATCH', { nickname: '관리자', body: '수정한 관리자 댓글', version: 1 })).status).toBe(200);
8081
expect(db.prepare('SELECT nickname, body FROM comments WHERE id=?').get(id)).toMatchObject({ nickname: '관리자', body: '수정한 관리자 댓글' });
8182
expect((await adminCall(`/${id}`, 'DELETE', { version: 2 })).status).toBe(200);

‎admin/worker/comments.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ async function handle(request: Request, env: Env): Promise<Response> {
230230
const stored = await env.DB.prepare('SELECT password_hash, password_salt, nickname, body, visibility, version FROM comments WHERE id=?').bind(id).first<{ password_hash: string; password_salt: string; nickname: string; body: string; visibility: string; version: number }>();
231231
if (!stored) return error('NOT_FOUND', '삭제되었거나 존재하지 않는 댓글입니다.', 404);
232232
const administratorMutation = (edit || remove) && await hasAdministratorMutationAccess(request, env);
233+
if ((edit || remove) && reservedNickname(stored.nickname) && !administratorMutation) return error('ADMIN_COMMENT_FORBIDDEN', '관리자 댓글은 수정하거나 삭제할 수 없습니다.', 403);
233234
if (!administratorMutation && (!validPassword(input.password) || (!reveal && !Number.isInteger(input.version)))) return error('INVALID_INPUT', '댓글 비밀번호를 입력해 주세요.', 422);
234235
if (edit && !validComment(input)) return error('INVALID_INPUT', '댓글 내용을 확인해 주세요.', 422);
235236
if (edit && (reservedNickname(stored.nickname) ? !reservedNickname(input.nickname) : reservedNickname(input.nickname))) return error('INVALID_INPUT', '관리자 닉네임은 사용할 수 없습니다.', 422);

‎src/shared/scripts/comments.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export function initializeComments() {
104104
const date = new Intl.DateTimeFormat('ko-KR', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(item.createdAt));
105105
const privateComment = item.visibility === 'private' && !item.body;
106106
const administratorComment = !privateComment && item.nickname === '관리자';
107-
const controls = '<button type="button" data-action="edit">수정</button><button type="button" data-action="delete">삭제</button>';
107+
const controls = isAdministrator || !administratorComment ? '<button type="button" data-action="edit">수정</button><button type="button" data-action="delete">삭제</button>' : '';
108108
node.innerHTML = `<div class="comment-meta"><strong${administratorComment ? ' class="comment-author--administrator"' : ''}>${privateComment ? '비공개' : escapeHtml(item.nickname ?? '')}</strong><time datetime="${escapeHtml(item.createdAt)}">${escapeHtml(date)}${item.version > 1 ? ' · 수정됨' : ''}</time></div>
109109
<p class="comment-text${privateComment ? ' comment-text--private' : ''}">${privateComment ? '🔒 비공개 댓글입니다.' : escapeHtml(item.body ?? '')}</p>
110110
<div class="comment-actions"><button class="comment-like" type="button" data-action="like" aria-pressed="${Boolean(item.liked)}" aria-label="좋아요 ${item.likes}개${item.liked ? ', 취소하기' : ''}"><span aria-hidden="true">${item.liked ? '♥' : '♡'}</span> 좋아요 <b>${item.likes}</b></button>${privateComment ? '<button type="button" data-action="reveal">내용 보기</button>' : ''}${!item.parentId ? '<button type="button" data-action="reply">답글</button>' : ''}${controls}</div><p class="comment-status" data-entry-status role="status"></p><div class="comment-replies" data-replies></div>`;

‎tests/e2e/release-ui.spec.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,19 @@ test('an authenticated administrator writes post comments and replies without an
7878

7979
test('a visitor can enter a password to edit or delete a comment', async ({ page }) => {
8080
const comment = { id: '11111111-1111-4111-8111-111111111111', page: '/blog/posts/git-reset-vs-git-revert/', parentId: null, nickname: '방문자', body: '좋은 글입니다.', visibility: 'public', version: 1, createdAt: '2026-09-11T00:00:00.000Z', updatedAt: '2026-09-11T00:00:00.000Z', likes: 0, liked: false };
81+
const administratorComment = { ...comment, id: '22222222-2222-4222-8222-222222222222', nickname: '관리자', body: '관리자 답글입니다.' };
8182
await page.route('**/api/comments/post-like?**', route => route.fulfill({ json: { likes: 0, liked: false } }));
82-
await page.route('**/api/comments?**', route => route.fulfill({ json: { items: [comment], total: 1, next: null, admin: false } }));
83+
await page.route('**/api/comments?**', route => route.fulfill({ json: { items: [comment, administratorComment], total: 2, next: null, admin: false } }));
8384
await page.goto('/blog/posts/git-reset-vs-git-revert/');
8485

8586
const entry = page.locator(`[data-comment-id="${comment.id}"]`);
8687
await expect(entry.locator(':scope > .comment-actions [data-action=edit]')).toHaveCount(1);
8788
await expect(entry.locator(':scope > .comment-actions [data-action=delete]')).toHaveCount(1);
8889
await entry.locator('[data-action=edit]').click();
8990
await expect(entry.locator('.comment-editor [name=password]')).toBeVisible();
91+
const administratorEntry = page.locator(`[data-comment-id="${administratorComment.id}"]`);
92+
await expect(administratorEntry.locator(':scope > .comment-actions [data-action=edit]')).toHaveCount(0);
93+
await expect(administratorEntry.locator(':scope > .comment-actions [data-action=delete]')).toHaveCount(0);
9094
});
9195

9296
test('guestbook has a separate anonymous comment page', async ({ page }) => {

0 commit comments

Comments
 (0)