mirror of
https://github.com/gethomepage/homepage.git
synced 2026-10-03 01:11:17 -07:00
DId: segment parsing hardening
This commit is contained in:
@@ -193,42 +193,61 @@ describe("pages/api/services/proxy", () => {
|
|||||||
widgets.linkwarden.mappings.collections.method = originalMethod;
|
widgets.linkwarden.mappings.collections.method = originalMethod;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("replaces endpoint segments and rejects unsupported segment keys/values", async () => {
|
it("replaces and encodes endpoint segments", async () => {
|
||||||
getServiceWidget.mockResolvedValue({ type: "segments" });
|
getServiceWidget.mockResolvedValue({ type: "segments" });
|
||||||
handlerFn.handler.mockImplementation(async (req, res) => res.status(200).json({ endpoint: req.query.endpoint }));
|
handlerFn.handler.mockImplementation(async (req, res) => res.status(200).json({ endpoint: req.query.endpoint }));
|
||||||
|
|
||||||
const res1 = createMockRes();
|
const res = createMockRes();
|
||||||
await servicesProxy(
|
await servicesProxy(
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
query: { group: "g", service: "s", index: "0", endpoint: "item", segments: JSON.stringify({ id: "123" }) },
|
query: {
|
||||||
|
group: "g",
|
||||||
|
service: "s",
|
||||||
|
index: "0",
|
||||||
|
endpoint: "item",
|
||||||
|
segments: JSON.stringify({ id: "session:123" }),
|
||||||
},
|
},
|
||||||
res1,
|
},
|
||||||
|
res,
|
||||||
);
|
);
|
||||||
expect(res1.statusCode).toBe(200);
|
expect(res.statusCode).toBe(200);
|
||||||
expect(res1.body).toEqual({ endpoint: "items/123" });
|
expect(res.body).toEqual({ endpoint: "items/session%3A123" });
|
||||||
|
});
|
||||||
|
|
||||||
const res2 = createMockRes();
|
it.each([
|
||||||
await servicesProxy(
|
["omitted segments", undefined],
|
||||||
{
|
["duplicate segment parameters", [JSON.stringify({ id: "123" }), JSON.stringify({ id: "456" })]],
|
||||||
method: "GET",
|
["unsupported keys", JSON.stringify({ nope: "123" })],
|
||||||
query: { group: "g", service: "s", index: "0", endpoint: "item", segments: JSON.stringify({ nope: "123" }) },
|
["missing keys", JSON.stringify({})],
|
||||||
},
|
["extra keys", JSON.stringify({ id: "123", extra: "value" })],
|
||||||
res2,
|
["empty values", JSON.stringify({ id: "" })],
|
||||||
);
|
["non-string values", JSON.stringify({ id: 123 })],
|
||||||
expect(res2.statusCode).toBe(403);
|
["literal traversal", JSON.stringify({ id: "../123" })],
|
||||||
expect(res2.body).toEqual({ error: "Unsupported segment" });
|
["encoded traversal", JSON.stringify({ id: "%2e%2e%2f123" })],
|
||||||
|
["double-encoded traversal", JSON.stringify({ id: "%252e%252e%252f123" })],
|
||||||
|
["encoded slash", JSON.stringify({ id: "session%2f123" })],
|
||||||
|
["encoded backslash", JSON.stringify({ id: "session%5c123" })],
|
||||||
|
["malformed encoding", JSON.stringify({ id: "%ZZ" })],
|
||||||
|
["invalid JSON", "{"],
|
||||||
|
["arrays", JSON.stringify(["123"])],
|
||||||
|
["null", "null"],
|
||||||
|
])("rejects %s in endpoint segments", async (_, segments) => {
|
||||||
|
getServiceWidget.mockResolvedValue({ type: "segments" });
|
||||||
|
handlerFn.handler.mockImplementation(async (req, res) => res.status(200).json({ endpoint: req.query.endpoint }));
|
||||||
|
|
||||||
const res3 = createMockRes();
|
const res = createMockRes();
|
||||||
await servicesProxy(
|
await servicesProxy(
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
query: { group: "g", service: "s", index: "0", endpoint: "item", segments: JSON.stringify({ id: "../123" }) },
|
query: { group: "g", service: "s", index: "0", endpoint: "item", segments },
|
||||||
},
|
},
|
||||||
res3,
|
res,
|
||||||
);
|
);
|
||||||
expect(res3.statusCode).toBe(403);
|
|
||||||
expect(res3.body).toEqual({ error: "Unsupported segment" });
|
expect(res.statusCode).toBe(403);
|
||||||
|
expect(res.body).toEqual({ error: "Unsupported segment" });
|
||||||
|
expect(handlerFn.handler).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds query params based on mapping params + optionalParams", async () => {
|
it("adds query params based on mapping params + optionalParams", async () => {
|
||||||
|
|||||||
@@ -7,6 +7,40 @@ import widgets from "widgets/widgets";
|
|||||||
|
|
||||||
const logger = createLogger("servicesProxy");
|
const logger = createLogger("servicesProxy");
|
||||||
|
|
||||||
|
function getSafeSegments(rawSegments, allowedSegments) {
|
||||||
|
if (typeof rawSegments !== "string" || !Array.isArray(allowedSegments)) return null;
|
||||||
|
|
||||||
|
let segments;
|
||||||
|
try {
|
||||||
|
segments = JSON.parse(rawSegments);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!segments || typeof segments !== "object" || Array.isArray(segments)) return null;
|
||||||
|
|
||||||
|
const keys = Object.keys(segments);
|
||||||
|
if (keys.length !== allowedSegments.length || !keys.every((key) => allowedSegments.includes(key))) return null;
|
||||||
|
|
||||||
|
const safeSegments = {};
|
||||||
|
for (const key of allowedSegments) {
|
||||||
|
const value = segments[key];
|
||||||
|
if (
|
||||||
|
typeof value !== "string" ||
|
||||||
|
value.length === 0 ||
|
||||||
|
value.includes("%") ||
|
||||||
|
value.includes("/") ||
|
||||||
|
value.includes("\\") ||
|
||||||
|
value.includes("..")
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
safeSegments[key] = encodeURIComponent(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeSegments;
|
||||||
|
}
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
try {
|
try {
|
||||||
const { service, group, index } = req.query;
|
const { service, group, index } = req.query;
|
||||||
@@ -55,19 +89,12 @@ export default async function handler(req, res) {
|
|||||||
if (mapping?.body) req.body = mapping?.body;
|
if (mapping?.body) req.body = mapping?.body;
|
||||||
req.query.endpoint = endpoint;
|
req.query.endpoint = endpoint;
|
||||||
|
|
||||||
if (req.query.segments) {
|
if (mapping.segments || req.query.segments) {
|
||||||
const segments = JSON.parse(req.query.segments);
|
const segments = getSafeSegments(req.query.segments, mapping.segments);
|
||||||
let validSegments = true;
|
if (!segments) {
|
||||||
Object.keys(segments).forEach((key) => {
|
logger.debug("Unsupported segments");
|
||||||
if (!mapping.segments.includes(key)) {
|
return res.status(403).json({ error: "Unsupported segment" });
|
||||||
logger.debug("Unsupported segment: %s", key);
|
|
||||||
validSegments = false;
|
|
||||||
} else if (segments[key].includes("/") || segments[key].includes("\\") || segments[key].includes("..")) {
|
|
||||||
logger.debug("Unsupported segment value: %s", segments[key]);
|
|
||||||
validSegments = false;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
if (!validSegments) return res.status(403).json({ error: "Unsupported segment" });
|
|
||||||
req.query.endpoint = formatApiCall(endpoint, segments);
|
req.query.endpoint = formatApiCall(endpoint, segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user