Craft CMS: Authorship spoofing in `entries/save-entry` via pre-check/post-mutation authorization gap
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
30일 내 악용 확률 예측
실측 악용 기록 없음
계획된 패치 주기 내 조치(60일 이내)
CVSS 벡터 · 메트릭
CVSS 벡터 정보 없음
상세 설명
Summary
EntriesController::actionSaveEntry() performs entry-edit permission checks before request-controlled author changes are applied to the model. The subsequent author mutation path accepts attacker-supplied authors / author parameters and allows the change when the current user is one of the old authors. Because the controller does not re-run authorization after mutating the author list, a low-privileged user can reassign an entry’s authorship to another user without holding the dedicated peer-author-change permission.
Details
The control flow begins in EntriesController.php:249. actionSaveEntry() loads the entry and enforces edit permissions before calling _populateEntryModel():
1public function actionSaveEntry(bool $duplicate = false): ?Response 2{ 3 ... 4 $entry = $this->_editableEntry($this->request->getBodyParam('entryId'), $siteId); 5 ... 6 $this->enforceEditEntryPermissions($entry, $duplicate); 7 ... 8 $this->_populateEntryModel($entry); 9 ...10 $success = Craft::$app->getElements()->saveElement($entry);11}The attacker-controlled source is in EntriesController.php:588:
1$entry->setAttributesFromRequest(array_filter([ 2 'authorIds' => $this->request->getBodyParam('authors') ?? 3 $this->request->getBodyParam('author') ?? 4 $entry->getAuthorId() ?? 5 static::currentUser()->id, 6]));Entry::setAttributesFromRequest() in Entry.php:1124 extracts the new author IDs and applies them if canChangeAuthor() returns true:
1if ( 2 ($authorIds !== null || $authorId !== null) && 3 $this->canChangeAuthor() 4) { 5 $this->_oldAuthorIds = $oldAuthorIds; 6 $this->setAuthorIds($authorIds); 7}canChangeAuthor() at Entry.php:2789 allows the author change when the current user can view peer entries and is already one of the existing authors:
1return ( 2 empty($authorIds) || 3 in_array($user->id, $authorIds) || 4 $user->can("changeAuthorForPeerEntries:$section->uid") 5);After the author list is mutated, the controller does not re-check authorization.
This closes the exploit chain:
- External source: authenticated request to
entries/save-entrywith attacker-controlledauthors[]. - Trust boundary failure: authorization is checked on the pre-mutation entry state, not on the post-mutation author assignment.
- Privileged sink: the author relationship is rewritten in persistent storage.
Preconditions derived from the source:
- The attacker is authenticated and can edit entry
345. - The attacker is among the existing authors of entry
345, or otherwise satisfiescanChangeAuthor()through the old author set. - The attacker has
viewPeerEntriesfor the section. - User ID
1exists and can be assigned as an author in that section.
Result:
enforceEditEntryPermissions()succeeds on the original entry state._populateEntryModel()readsauthors[]=1from the request body.setAttributesFromRequest()updatesauthorIdsbecausecanChangeAuthor()is evaluated against the old authorship state.saveElement()persists the change and_saveAuthors()rewrites the entry-author relation.- Entry
345now appears authored by user1.
Impact
This allows low-privileged users to falsify content ownership and alter the authorship of entries without having the dedicated author-management permission. The impact includes corrupted audit trails, misleading notifications, broken approval workflows, and unauthorized reassignment of content responsibility.
AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.