mirror of
https://github.com/karl0ss/homepage.git
synced 2025-04-29 12:03:41 +01:00
Merge pull request from GHSA-24m5-7vjx-9x37
* Restrict emby endpoints and proxy segments * Dont allow path traversal in segments * Restrict qbittorrent proxy endpoints * Restrict npm proxy endpoints * Restrict flood proxy endpoints * Restrict tdarr proxy endpoints * Restrict xteve proxy endpoints * Restrict transmission proxy endpoints * disallow non-mapped endpoints this change drops all requests that have un-mapped endpoint queries allowedEndpoints is added as a method to pass proxy requests via a regex on the endpoint most widgets with custom proxies use either no endpoint, or a static one Co-Authored-By: Ben Phelps <ben@phelps.io>
This commit is contained in:
parent
8823b04291
commit
b3cf985d4a
@ -18,6 +18,11 @@ export default async function handler(req, res) {
|
|||||||
const serviceProxyHandler = widget.proxyHandler || genericProxyHandler;
|
const serviceProxyHandler = widget.proxyHandler || genericProxyHandler;
|
||||||
|
|
||||||
if (serviceProxyHandler instanceof Function) {
|
if (serviceProxyHandler instanceof Function) {
|
||||||
|
// quick return for no endpoint services
|
||||||
|
if (!req.query.endpoint) {
|
||||||
|
return serviceProxyHandler(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
// map opaque endpoints to their actual endpoint
|
// map opaque endpoints to their actual endpoint
|
||||||
if (widget?.mappings) {
|
if (widget?.mappings) {
|
||||||
const mapping = widget?.mappings?.[req.query.endpoint];
|
const mapping = widget?.mappings?.[req.query.endpoint];
|
||||||
@ -38,6 +43,15 @@ export default async function handler(req, res) {
|
|||||||
|
|
||||||
if (req.query.segments) {
|
if (req.query.segments) {
|
||||||
const segments = JSON.parse(req.query.segments);
|
const segments = JSON.parse(req.query.segments);
|
||||||
|
for (const key in segments) {
|
||||||
|
if (!mapping.segments.includes(key)) {
|
||||||
|
logger.debug("Unsupported segment: %s", key);
|
||||||
|
return res.status(403).json({ error: "Unsupported segment" });
|
||||||
|
} else if (segments[key].includes("/")) {
|
||||||
|
logger.debug("Unsupported segment value: %s", segments[key]);
|
||||||
|
return res.status(403).json({ error: "Unsupported segment value" });
|
||||||
|
}
|
||||||
|
}
|
||||||
req.query.endpoint = formatApiCall(endpoint, segments);
|
req.query.endpoint = formatApiCall(endpoint, segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +80,14 @@ export default async function handler(req, res) {
|
|||||||
return serviceProxyHandler(req, res, map);
|
return serviceProxyHandler(req, res, map);
|
||||||
}
|
}
|
||||||
|
|
||||||
return serviceProxyHandler(req, res);
|
if (widget.allowedEndpoints instanceof RegExp) {
|
||||||
|
if (widget.allowedEndpoints.test(req.query.endpoint)) {
|
||||||
|
return serviceProxyHandler(req, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Unmapped proxy request.");
|
||||||
|
return res.status(403).json({ error: "Unmapped proxy request." });
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("Unknown proxy service type: %s", type);
|
logger.debug("Unknown proxy service type: %s", type);
|
||||||
|
@ -8,22 +8,16 @@ export function formatApiCall(url, args) {
|
|||||||
return url.replace(/\/+$/, "").replace(find, replace).replace(find, replace);
|
return url.replace(/\/+$/, "").replace(find, replace).replace(find, replace);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getURLSearchParams(widget, endpoint) {
|
export function getURLSearchParams(widget, endpoint) {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
type: widget.type,
|
type: widget.type,
|
||||||
group: widget.service_group,
|
group: widget.service_group,
|
||||||
service: widget.service_name,
|
service: widget.service_name,
|
||||||
endpoint,
|
|
||||||
});
|
});
|
||||||
return params;
|
if (endpoint) {
|
||||||
}
|
params.append("endpoint", endpoint);
|
||||||
|
|
||||||
export function formatProxyUrlWithSegments(widget, endpoint, segments) {
|
|
||||||
const params = getURLSearchParams(widget, endpoint);
|
|
||||||
if (segments) {
|
|
||||||
params.append("segments", JSON.stringify(segments));
|
|
||||||
}
|
}
|
||||||
return `/api/services/proxy?${params.toString()}`;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatProxyUrl(widget, endpoint, queryParams) {
|
export function formatProxyUrl(widget, endpoint, queryParams) {
|
||||||
|
@ -4,7 +4,7 @@ import { MdOutlineSmartDisplay } from "react-icons/md";
|
|||||||
|
|
||||||
import Block from "components/services/widget/block";
|
import Block from "components/services/widget/block";
|
||||||
import Container from "components/services/widget/container";
|
import Container from "components/services/widget/container";
|
||||||
import { formatProxyUrlWithSegments } from "utils/proxy/api-helpers";
|
import { getURLSearchParams } from "utils/proxy/api-helpers";
|
||||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||||
|
|
||||||
function ticksToTime(ticks) {
|
function ticksToTime(ticks) {
|
||||||
@ -217,10 +217,14 @@ export default function Component({ service }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function handlePlayCommand(session, command) {
|
async function handlePlayCommand(session, command) {
|
||||||
const url = formatProxyUrlWithSegments(widget, "PlayControl", {
|
const params = getURLSearchParams(widget, command);
|
||||||
sessionId: session.Id,
|
params.append(
|
||||||
command,
|
"segments",
|
||||||
});
|
JSON.stringify({
|
||||||
|
sessionId: session.Id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const url = `/api/services/proxy?${params.toString()}`;
|
||||||
await fetch(url).then(() => {
|
await fetch(url).then(() => {
|
||||||
sessionMutate();
|
sessionMutate();
|
||||||
});
|
});
|
||||||
|
@ -10,12 +10,16 @@ const widget = {
|
|||||||
},
|
},
|
||||||
Count: {
|
Count: {
|
||||||
endpoint: "Items/Counts",
|
endpoint: "Items/Counts",
|
||||||
segments: ["MovieCount", "SeriesCount", "EpisodeCount", "SongCount"],
|
|
||||||
},
|
},
|
||||||
PlayControl: {
|
Unpause: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
endpoint: "Sessions/{sessionId}/Playing/{command}",
|
endpoint: "Sessions/{sessionId}/Playing/Unpause",
|
||||||
segments: ["sessionId", "command"],
|
segments: ["sessionId"],
|
||||||
|
},
|
||||||
|
Pause: {
|
||||||
|
method: "POST",
|
||||||
|
endpoint: "Sessions/{sessionId}/Playing/Pause",
|
||||||
|
segments: ["sessionId"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,12 @@ import floodProxyHandler from "./proxy";
|
|||||||
|
|
||||||
const widget = {
|
const widget = {
|
||||||
proxyHandler: floodProxyHandler,
|
proxyHandler: floodProxyHandler,
|
||||||
|
|
||||||
|
mappings: {
|
||||||
|
torrents: {
|
||||||
|
endpoint: "torrents",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default widget;
|
export default widget;
|
||||||
|
@ -2,6 +2,7 @@ import fritzboxProxyHandler from "./proxy";
|
|||||||
|
|
||||||
const widget = {
|
const widget = {
|
||||||
proxyHandler: fritzboxProxyHandler,
|
proxyHandler: fritzboxProxyHandler,
|
||||||
|
allowedEndpoints: /status/,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default widget;
|
export default widget;
|
||||||
|
@ -2,6 +2,7 @@ import gamedigProxyHandler from "./proxy";
|
|||||||
|
|
||||||
const widget = {
|
const widget = {
|
||||||
proxyHandler: gamedigProxyHandler,
|
proxyHandler: gamedigProxyHandler,
|
||||||
|
allowedEndpoints: /status/,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default widget;
|
export default widget;
|
||||||
|
@ -3,6 +3,7 @@ import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
|||||||
const widget = {
|
const widget = {
|
||||||
api: "{url}/api/{endpoint}",
|
api: "{url}/api/{endpoint}",
|
||||||
proxyHandler: credentialedProxyHandler,
|
proxyHandler: credentialedProxyHandler,
|
||||||
|
allowedEndpoints: /\d\/quicklook|diskio|fs|gpu|system|mem|network|processlist|sensors/,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default widget;
|
export default widget;
|
||||||
|
@ -2,6 +2,7 @@ import minecraftProxyHandler from "./proxy";
|
|||||||
|
|
||||||
const widget = {
|
const widget = {
|
||||||
proxyHandler: minecraftProxyHandler,
|
proxyHandler: minecraftProxyHandler,
|
||||||
|
allowedEndpoints: /status/,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default widget;
|
export default widget;
|
||||||
|
@ -5,7 +5,7 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
|
|||||||
export default function Component({ service }) {
|
export default function Component({ service }) {
|
||||||
const { widget } = service;
|
const { widget } = service;
|
||||||
|
|
||||||
const { data: infoData, error: infoError } = useWidgetAPI(widget, "nginx/proxy-hosts");
|
const { data: infoData, error: infoError } = useWidgetAPI(widget, "hosts");
|
||||||
|
|
||||||
if (infoError) {
|
if (infoError) {
|
||||||
return <Container service={service} error={infoError} />;
|
return <Container service={service} error={infoError} />;
|
||||||
|
@ -3,6 +3,12 @@ import npmProxyHandler from "./proxy";
|
|||||||
const widget = {
|
const widget = {
|
||||||
api: "{url}/api/{endpoint}",
|
api: "{url}/api/{endpoint}",
|
||||||
proxyHandler: npmProxyHandler,
|
proxyHandler: npmProxyHandler,
|
||||||
|
|
||||||
|
mappings: {
|
||||||
|
hosts: {
|
||||||
|
endpoint: "nginx/proxy-hosts",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default widget;
|
export default widget;
|
||||||
|
@ -3,6 +3,7 @@ import jsonrpcProxyHandler from "utils/proxy/handlers/jsonrpc";
|
|||||||
const widget = {
|
const widget = {
|
||||||
api: "{url}/jsonrpc",
|
api: "{url}/jsonrpc",
|
||||||
proxyHandler: jsonrpcProxyHandler,
|
proxyHandler: jsonrpcProxyHandler,
|
||||||
|
allowedEndpoints: /status/,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default widget;
|
export default widget;
|
||||||
|
@ -9,7 +9,7 @@ export default function Component({ service }) {
|
|||||||
|
|
||||||
const { widget } = service;
|
const { widget } = service;
|
||||||
|
|
||||||
const { data: torrentData, error: torrentError } = useWidgetAPI(widget, "torrents/info");
|
const { data: torrentData, error: torrentError } = useWidgetAPI(widget, "torrents");
|
||||||
|
|
||||||
if (torrentError) {
|
if (torrentError) {
|
||||||
return <Container service={service} error={torrentError} />;
|
return <Container service={service} error={torrentError} />;
|
||||||
|
@ -2,6 +2,12 @@ import qbittorrentProxyHandler from "./proxy";
|
|||||||
|
|
||||||
const widget = {
|
const widget = {
|
||||||
proxyHandler: qbittorrentProxyHandler,
|
proxyHandler: qbittorrentProxyHandler,
|
||||||
|
|
||||||
|
mappings: {
|
||||||
|
torrents: {
|
||||||
|
endpoint: "torrents/info",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default widget;
|
export default widget;
|
||||||
|
@ -3,6 +3,7 @@ import qnapProxyHandler from "./proxy";
|
|||||||
const widget = {
|
const widget = {
|
||||||
api: "{url}",
|
api: "{url}",
|
||||||
proxyHandler: qnapProxyHandler,
|
proxyHandler: qnapProxyHandler,
|
||||||
|
allowedEndpoints: /status/,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default widget;
|
export default widget;
|
||||||
|
@ -3,6 +3,7 @@ import genericProxyHandler from "utils/proxy/handlers/generic";
|
|||||||
const widget = {
|
const widget = {
|
||||||
api: "{url}/?stats=true",
|
api: "{url}/?stats=true",
|
||||||
proxyHandler: genericProxyHandler,
|
proxyHandler: genericProxyHandler,
|
||||||
|
allowedEndpoints: /overview/,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default widget;
|
export default widget;
|
||||||
|
@ -8,7 +8,7 @@ const proxyName = "tdarrProxyHandler";
|
|||||||
const logger = createLogger(proxyName);
|
const logger = createLogger(proxyName);
|
||||||
|
|
||||||
export default async function tdarrProxyHandler(req, res) {
|
export default async function tdarrProxyHandler(req, res) {
|
||||||
const { group, service, endpoint } = req.query;
|
const { group, service } = req.query;
|
||||||
|
|
||||||
if (!group || !service) {
|
if (!group || !service) {
|
||||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||||
@ -22,7 +22,7 @@ export default async function tdarrProxyHandler(req, res) {
|
|||||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
|
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint: undefined, ...widget }));
|
||||||
|
|
||||||
const [status, contentType, data] = await httpProxy(url, {
|
const [status, contentType, data] = await httpProxy(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -11,7 +11,7 @@ const headerCacheKey = `${proxyName}__headers`;
|
|||||||
const logger = createLogger(proxyName);
|
const logger = createLogger(proxyName);
|
||||||
|
|
||||||
export default async function transmissionProxyHandler(req, res) {
|
export default async function transmissionProxyHandler(req, res) {
|
||||||
const { group, service, endpoint } = req.query;
|
const { group, service } = req.query;
|
||||||
|
|
||||||
if (!group || !service) {
|
if (!group || !service) {
|
||||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||||
@ -35,7 +35,7 @@ export default async function transmissionProxyHandler(req, res) {
|
|||||||
|
|
||||||
const api = `${widget.url}${widget.rpcUrl || widgets[widget.type].rpcUrl}rpc`;
|
const api = `${widget.url}${widget.rpcUrl || widgets[widget.type].rpcUrl}rpc`;
|
||||||
|
|
||||||
const url = new URL(formatApiCall(api, { endpoint, ...widget }));
|
const url = new URL(formatApiCall(api, { endpoint: undefined, ...widget }));
|
||||||
const csrfHeaderName = "x-transmission-session-id";
|
const csrfHeaderName = "x-transmission-session-id";
|
||||||
|
|
||||||
const method = "POST";
|
const method = "POST";
|
||||||
|
@ -2,6 +2,7 @@ import urbackupProxyHandler from "./proxy";
|
|||||||
|
|
||||||
const widget = {
|
const widget = {
|
||||||
proxyHandler: urbackupProxyHandler,
|
proxyHandler: urbackupProxyHandler,
|
||||||
|
allowedEndpoints: /status/,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default widget;
|
export default widget;
|
||||||
|
@ -9,7 +9,7 @@ export default function Component({ service }) {
|
|||||||
|
|
||||||
const { widget } = service;
|
const { widget } = service;
|
||||||
|
|
||||||
const { data: xteveData, error: xteveError } = useWidgetAPI(widget, "api");
|
const { data: xteveData, error: xteveError } = useWidgetAPI(widget);
|
||||||
|
|
||||||
if (xteveError) {
|
if (xteveError) {
|
||||||
return <Container service={service} error={xteveError} />;
|
return <Container service={service} error={xteveError} />;
|
||||||
|
@ -7,7 +7,7 @@ import getServiceWidget from "utils/config/service-helpers";
|
|||||||
const logger = createLogger("xteveProxyHandler");
|
const logger = createLogger("xteveProxyHandler");
|
||||||
|
|
||||||
export default async function xteveProxyHandler(req, res) {
|
export default async function xteveProxyHandler(req, res) {
|
||||||
const { group, service, endpoint } = req.query;
|
const { group, service } = req.query;
|
||||||
|
|
||||||
if (!group || !service) {
|
if (!group || !service) {
|
||||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||||
@ -19,7 +19,7 @@ export default async function xteveProxyHandler(req, res) {
|
|||||||
return res.status(403).json({ error: "Service does not support API calls" });
|
return res.status(403).json({ error: "Service does not support API calls" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = formatApiCall(api, { endpoint, ...widget });
|
const url = formatApiCall(api, { endpoint: "api/", ...widget });
|
||||||
const method = "POST";
|
const method = "POST";
|
||||||
const payload = { cmd: "status" };
|
const payload = { cmd: "status" };
|
||||||
|
|
||||||
|
@ -3,12 +3,6 @@ import xteveProxyHandler from "./proxy";
|
|||||||
const widget = {
|
const widget = {
|
||||||
api: "{url}/{endpoint}",
|
api: "{url}/{endpoint}",
|
||||||
proxyHandler: xteveProxyHandler,
|
proxyHandler: xteveProxyHandler,
|
||||||
|
|
||||||
mappings: {
|
|
||||||
api: {
|
|
||||||
endpoint: "api/",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default widget;
|
export default widget;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user