security(alerts): authorize the apps an alert targets, not just who owns it - #7872
Open
ar2rsawseen wants to merge 1 commit into
Open
security(alerts): authorize the apps an alert targets, not just who owns it#7872ar2rsawseen wants to merge 1 commit into
ar2rsawseen wants to merge 1 commit into
Conversation
…wns it The alert endpoints authorize the caller against params.qstring.app_id, and the create path checks the submitted selectedApps. Neither says anything about the apps the stored alert points at, and an update may omit selectedApps to keep whatever is stored. The by-id mutation paths filtered only on _id and createdBy, which is ownership rather than authorization: a member who created an alert for one app, then lost access to it while keeping alerts rights elsewhere, could still edit and re-enable that alert by sending the request with an app they do still hold. /o/alert/list was already fixed for this and hides such alerts, so the write side was the remaining half. Three changes: update by id now loads the alert and requires update rights on every app it targets, in addition to the existing check on any newly submitted selectedApps the status endpoint requires the same before switching an alert ON. Switching one OFF stays allowed, because it only reduces what the alert does and refusing would leave somebody who lost access unable to stop mail they no longer want the scheduled job checks the owner still has read access to the app before sending. Nothing re-checked anything once an alert was enabled, so one created before access was revoked kept emailing that app's metrics indefinitely with no further action by anybody. Fixing only the endpoints would have left that delete deliberately stays owner-only. It cannot reach another app's data and the owner needs to be able to clean up an alert they can no longer manage. The shared decision lives in parts/app-authorization.js because the request path and the job both need it, including the legacy allowance: members with no permission object reach apps through user_of, and the right helpers fall through to admin_of alone, so a strict reading would both stop their alerts and lock them out of their own configuration. /o/alert/list already makes that allowance and all three now agree. An alert with no resolvable owner is reported and still sent, rather than dropped. Refusing would silently stop alerts that have worked for years, which is worse than the exposure being closed here. Tests cover both directions. 19 unit tests on the authorization module, including every legacy case, and API tests through the real endpoints for the exploit and for the happy paths: the member can still create, update, enable and disable their own alerts on apps they hold, can still switch off and delete the alert for the revoked app, and a global admin is unaffected. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The alert endpoints authorize the caller against
params.qstring.app_id, and the create path checks the submittedselectedApps. Neither says anything about the apps the stored alert points at, and an update may omitselectedAppsto keep whatever is stored.The by-id mutation paths filtered only on
_idandcreatedBy. That is ownership, not authorization: a member who created an alert for one app and later lost access to it, while keeping alerts rights elsewhere, could still edit and re-enable that alert by sending the request with an app they do still hold./o/alert/listwas already fixed for this and hides such alerts, so the write side was the remaining half.Three changes
Update by id loads the alert and requires update rights on every app it targets, on top of the existing check on any newly submitted
selectedApps.The status endpoint requires the same before switching an alert on. Switching one off stays allowed: it only reduces what the alert does, and refusing would leave somebody who lost access unable to stop mail they no longer want, which is worse than the problem being fixed.
The scheduled job checks the owner still has read access before sending. Nothing re-checked anything once an alert was enabled, so one created before access was revoked kept emailing that app's metrics indefinitely, with no request from anybody. Fixing only the endpoints would have left that in place, and it is the more likely case in practice.
deletedeliberately stays owner-only. It cannot reach another app's data, and the owner needs to be able to clean up an alert they can no longer manage.The legacy trap this had to avoid
Members created before the
permissionobject reach apps throughuser_of, andhasUpdateRight/hasReadRightthen fall through toadmin_ofalone. A strict reading would judge them unauthorized, which would have locked them out of their own alerts and stopped delivery./o/alert/listalready makes that allowance, and now all three paths agree, otherwise an alert could be listed but not editable, or editable but not delivered.The shared decision lives in
parts/app-authorization.jsbecause the request path and the scheduled job both need it. Two copies of that allowance would drift.One deliberate fail-open
An alert with no resolvable
createdByis reported at warn level and still sent. Those predate the field, cannot be attributed, and refusing would silently stop alerts that have worked for years. That is a worse outcome than the exposure closed here, and it is visible in the log rather than silent.Tests, both directions
test/unit-tests/plugins.alerts.app-authorization.js, 19 cases: modern and legacy members,admin_ofvsuser_of, global admin, multi-app targets requiring all apps, empty target lists, missing members, and that read access does not confer update.plugins/alerts/tests.js, API tests through the real endpoints with a real member whose access is revoked mid-test.The exploit cases: the update with
selectedAppsomitted is refused, enabling is refused, the alert is verified unchanged afterwards, and it stays hidden from the member's list.The happy paths, which matter just as much: the member can still create, update, enable and disable their own alerts on the app they hold, can still switch off and delete the alert for the revoked app, and a global admin is unaffected.
Full unit suite: 204 passing. The 2 failures are
Countly Request, which makes real HTTP calls and fails without network on a clean checkout too.Note on scope
The status pre-check is scoped to the caller's own alerts, so an id belonging to somebody else keeps behaving as it did, a silent no-op, rather than returning a 403 that would confirm an alert with those apps exists.
🤖 Generated with Claude Code