Summary
Multiple notification API endpoints are registered without authentication middleware, while sibling endpoints in the same codebase correctly use ClusterKeyAuthorization.isAuthorizedServiceMiddleware. These endpoints are externally reachable via the Nginx proxy at /notification/. Combined with a projectId leak from the public Status Page API, an unauthenticated attacker can purchase phone numbers on the victim's Twilio account and delete all existing alerting numbers.
Root Cause
App/FeatureSet/Notification/API/PhoneNumber.ts registers 5 routes on the Express router with zero authentication middleware:
router.post("/search", ...); // line 31
router.post("/list-owned", ...); // line 155
router.post("/assign-existing", ...); // line 238
router.post("/purchase", ...); // line 386
router.delete("/release/:id", ...); // line 529
Compare with SMS.ts and Call.ts in the same directory, which correctly use ClusterKeyAuthorization.isAuthorizedServiceMiddleware on their routes.
The Nginx config at Nginx/default.conf.template:485-506 proxies /notification directly to the app server:
location /notification {
rewrite ^/notification(.*)$ /api/notification$1 break;
proxy_pass $backend_app;
}
No authentication at the proxy layer.
ProjectId Leak via Public Status Page
The phone number endpoints require a projectId (UUID) in the request body. This UUID is leaked by the public Status Page overview API.
Common/Server/API/StatusPageAPI.ts line 4435 selects projectId: true when fetching status page data. Line 2211 serializes the full statusPage object (including projectId) into the JSON response sent to unauthenticated status page viewers.
Any public OneUptime status page exposes its projectId in the overview API response.
Attack Chain
- Attacker visits a public OneUptime status page
- The overview API response contains
statusPage.projectId
POST /notification/phone-number/purchase with the leaked projectId buys phone numbers on the victim's Twilio account ($1-5+ per number, repeatable)
POST /notification/phone-number/list-owned with the projectId enumerates all owned phone numbers
DELETE /notification/phone-number/release/<id> releases each number, killing all call-based alerting
All database queries in PhoneNumber.ts use isRoot: true (lines 79, 189, 286, 305, 369, 426, 445, 512, 554, 598), bypassing all tenant access controls.
Additional Unauthenticated Endpoints
SMTP Config Test (App/FeatureSet/Notification/API/SMTPConfig.ts:25): Unauthenticated. Reads any project's SMTP configuration (hostname, port, username, password, OAuth credentials) using isRoot: true (line 39) and sends email through it. Requires guessing the smtpConfigId (UUID).
WhatsApp Test (App/FeatureSet/Notification/API/WhatsApp.ts:444): Unauthenticated. Sends WhatsApp messages from the organization's Business account to arbitrary phone numbers.
Impact
- Financial abuse: Unlimited phone number purchases on the victim's Twilio account. Each costs $1-5+.
- Service disruption: Delete all owned phone numbers. All call-based incident alerting stops.
- SMTP credential exposure: Read SMTP credentials for any project (requires config UUID).
- WhatsApp abuse: Send messages from the organization's WhatsApp Business account.
Suggested Fix
Add ClusterKeyAuthorization.isAuthorizedServiceMiddleware to all 5 PhoneNumber routes, the SMTPConfig test route, and the WhatsApp test route, matching the pattern used by SMS.ts and Call.ts.
Remove projectId from the Status Page overview API response select statement, or filter it from the serialized output.
Summary
Multiple notification API endpoints are registered without authentication middleware, while sibling endpoints in the same codebase correctly use
ClusterKeyAuthorization.isAuthorizedServiceMiddleware. These endpoints are externally reachable via the Nginx proxy at/notification/. Combined with aprojectIdleak from the public Status Page API, an unauthenticated attacker can purchase phone numbers on the victim's Twilio account and delete all existing alerting numbers.Root Cause
App/FeatureSet/Notification/API/PhoneNumber.tsregisters 5 routes on the Express router with zero authentication middleware:Compare with
SMS.tsandCall.tsin the same directory, which correctly useClusterKeyAuthorization.isAuthorizedServiceMiddlewareon their routes.The Nginx config at
Nginx/default.conf.template:485-506proxies/notificationdirectly to the app server:No authentication at the proxy layer.
ProjectId Leak via Public Status Page
The phone number endpoints require a
projectId(UUID) in the request body. This UUID is leaked by the public Status Page overview API.Common/Server/API/StatusPageAPI.tsline 4435 selectsprojectId: truewhen fetching status page data. Line 2211 serializes the fullstatusPageobject (includingprojectId) into the JSON response sent to unauthenticated status page viewers.Any public OneUptime status page exposes its
projectIdin the overview API response.Attack Chain
statusPage.projectIdPOST /notification/phone-number/purchasewith the leakedprojectIdbuys phone numbers on the victim's Twilio account ($1-5+ per number, repeatable)POST /notification/phone-number/list-ownedwith theprojectIdenumerates all owned phone numbersDELETE /notification/phone-number/release/<id>releases each number, killing all call-based alertingAll database queries in PhoneNumber.ts use
isRoot: true(lines 79, 189, 286, 305, 369, 426, 445, 512, 554, 598), bypassing all tenant access controls.Additional Unauthenticated Endpoints
SMTP Config Test (
App/FeatureSet/Notification/API/SMTPConfig.ts:25): Unauthenticated. Reads any project's SMTP configuration (hostname, port, username, password, OAuth credentials) usingisRoot: true(line 39) and sends email through it. Requires guessing thesmtpConfigId(UUID).WhatsApp Test (
App/FeatureSet/Notification/API/WhatsApp.ts:444): Unauthenticated. Sends WhatsApp messages from the organization's Business account to arbitrary phone numbers.Impact
Suggested Fix
Add
ClusterKeyAuthorization.isAuthorizedServiceMiddlewareto all 5 PhoneNumber routes, the SMTPConfig test route, and the WhatsApp test route, matching the pattern used by SMS.ts and Call.ts.Remove
projectIdfrom the Status Page overview API responseselectstatement, or filter it from the serialized output.