diff --git a/server/model/status_page.js b/server/model/status_page.js index 7087e4e09..0fef5caa8 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -30,6 +30,7 @@ class StatusPage extends BeanModel { ]); if (statusPage) { + response.type("application/rss+xml"); response.send(await StatusPage.renderRSS(statusPage, slug)); } else { response.status(404).send(UptimeKumaServer.getInstance().indexHTML); diff --git a/test/backend-test/test-status-page.js b/test/backend-test/test-status-page.js new file mode 100644 index 000000000..fe3fed2a5 --- /dev/null +++ b/test/backend-test/test-status-page.js @@ -0,0 +1,38 @@ +const { describe, test } = require("node:test"); +const assert = require("node:assert"); +const StatusPage = require("../../server/model/status_page"); +const { STATUS_PAGE_ALL_UP, STATUS_PAGE_ALL_DOWN, STATUS_PAGE_PARTIAL_DOWN, STATUS_PAGE_MAINTENANCE } = require("../../src/util"); + +describe("StatusPage", () => { + describe("getStatusDescription()", () => { + test("returns 'No Services' when status is -1", () => { + const description = StatusPage.getStatusDescription(-1); + assert.strictEqual(description, "No Services"); + }); + + test("returns 'All Systems Operational' when all services are up", () => { + const description = StatusPage.getStatusDescription(STATUS_PAGE_ALL_UP); + assert.strictEqual(description, "All Systems Operational"); + }); + + test("returns 'Partially Degraded Service' when some services are down", () => { + const description = StatusPage.getStatusDescription(STATUS_PAGE_PARTIAL_DOWN); + assert.strictEqual(description, "Partially Degraded Service"); + }); + + test("returns 'Degraded Service' when all services are down", () => { + const description = StatusPage.getStatusDescription(STATUS_PAGE_ALL_DOWN); + assert.strictEqual(description, "Degraded Service"); + }); + + test("returns 'Under maintenance' when status page is in maintenance", () => { + const description = StatusPage.getStatusDescription(STATUS_PAGE_MAINTENANCE); + assert.strictEqual(description, "Under maintenance"); + }); + + test("returns '?' for unknown status values", () => { + const description = StatusPage.getStatusDescription(999); + assert.strictEqual(description, "?"); + }); + }); +}); diff --git a/test/e2e/specs/status-page.spec.js b/test/e2e/specs/status-page.spec.js index 1964f92b5..1b86d5137 100644 --- a/test/e2e/specs/status-page.spec.js +++ b/test/e2e/specs/status-page.spec.js @@ -159,4 +159,86 @@ test.describe("Status Page", () => { // @todo Test certificate expiry // @todo Test domain names + test("RSS feed escapes malicious monitor names", async ({ page }, testInfo) => { + test.setTimeout(60000); + + // Test various XSS payloads in monitor names + const maliciousMonitorName1 = ""; + const maliciousMonitorName2 = "x"; + const normalMonitorName = "Production API Server"; + + await page.goto("./add"); + await login(page); + + // Create first monitor with script tag payload + await expect(page.getByTestId("monitor-type-select")).toBeVisible(); + await page.getByTestId("monitor-type-select").selectOption("http"); + await page.getByTestId("friendly-name-input").fill(maliciousMonitorName1); + await page.getByTestId("url-input").fill("https://malicious1.example.com"); + await page.getByTestId("save-button").click(); + await page.waitForURL("/dashboard/*"); + + // Create second monitor with title breakout payload + await page.goto("./add"); + await page.getByTestId("monitor-type-select").selectOption("http"); + await page.getByTestId("friendly-name-input").fill(maliciousMonitorName2); + await page.getByTestId("url-input").fill("https://malicious2.example.com"); + await page.getByTestId("save-button").click(); + await page.waitForURL("/dashboard/*"); + + // Create third monitor with normal name + await page.goto("./add"); + await page.getByTestId("monitor-type-select").selectOption("http"); + await page.getByTestId("friendly-name-input").fill(normalMonitorName); + await page.getByTestId("url-input").fill("https://normal.example.com"); + await page.getByTestId("save-button").click(); + await page.waitForURL("/dashboard/*"); + + // Create a status page + await page.goto("./add-status-page"); + await page.getByTestId("name-input").fill("Security Test"); + await page.getByTestId("slug-input").fill("security-test"); + await page.getByTestId("submit-button").click(); + await page.waitForURL("/status/security-test?edit"); + + // Add a group and all monitors + await page.getByTestId("add-group-button").click(); + await page.getByTestId("group-name").fill("Test Group"); + + // Add all three monitors + await page.getByTestId("monitor-select").click(); + await page.getByTestId("monitor-select").getByRole("option", { name: maliciousMonitorName1 }).click(); + await page.getByTestId("monitor-select").click(); + await page.getByTestId("monitor-select").getByRole("option", { name: maliciousMonitorName2 }).click(); + await page.getByTestId("monitor-select").click(); + await page.getByTestId("monitor-select").getByRole("option", { name: normalMonitorName }).click(); + + await page.getByTestId("save-button").click(); + await expect(page.getByTestId("edit-sidebar")).toHaveCount(0); + + // Fetch the RSS feed + const rssResponse = await page.request.get("/status/security-test/rss"); + expect(rssResponse.status()).toBe(200); + expect(rssResponse.headers()["content-type"]).toBe("application/rss+xml; charset=utf-8"); + expect(rssResponse.ok()).toBeTruthy(); + + const rssContent = await rssResponse.text(); + + // Attach RSS content for inspection + await testInfo.attach("rss-feed.xml", { + body: rssContent, + contentType: "application/xml" + }); + + // Verify all payloads are escaped using CDATA + expect(rssContent).toContain(`<title><![CDATA[${maliciousMonitorName1} is down]]>`); + expect(rssContent).toContain(`<![CDATA[${maliciousMonitorName2} is down]]>`); + expect(rssContent).toContain(`<![CDATA[${normalMonitorName} is down]]>`); + + // Verify RSS feed structure is valid + expect(rssContent).toContain(""); + }); + });