fix(rss): fix the rss endpoint having the wrong content type (#6570)

This commit is contained in:
Frank Elsinga 2026-01-02 17:02:31 +01:00 committed by GitHub
parent 71a17c9329
commit a36b365f4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 121 additions and 0 deletions

View File

@ -30,6 +30,7 @@ class StatusPage extends BeanModel {
]); ]);
if (statusPage) { if (statusPage) {
response.type("application/rss+xml");
response.send(await StatusPage.renderRSS(statusPage, slug)); response.send(await StatusPage.renderRSS(statusPage, slug));
} else { } else {
response.status(404).send(UptimeKumaServer.getInstance().indexHTML); response.status(404).send(UptimeKumaServer.getInstance().indexHTML);

View File

@ -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, "?");
});
});
});

View File

@ -159,4 +159,86 @@ test.describe("Status Page", () => {
// @todo Test certificate expiry // @todo Test certificate expiry
// @todo Test domain names // @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 = "<script>alert(1)</script>";
const maliciousMonitorName2 = "x</title><script>alert(document.domain)</script><title>";
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]]></title>`);
expect(rssContent).toContain(`<title><![CDATA[${maliciousMonitorName2} is down]]></title>`);
expect(rssContent).toContain(`<title><![CDATA[${normalMonitorName} is down]]></title>`);
// Verify RSS feed structure is valid
expect(rssContent).toContain("<?xml version=\"1.0\"");
expect(rssContent).toContain("<rss");
expect(rssContent).toContain("</rss>");
});
}); });