bug fixes.

This commit is contained in:
KingKDot
2026-05-18 23:07:11 -05:00
parent 59a2622b47
commit 423e41854f
14 changed files with 110 additions and 69 deletions
@@ -58,6 +58,20 @@ export const DEFAULT_TELEGRAM_TEMPLATE =
const NOTIFICATION_SCREENSHOT_WAIT_MS = 5_000;
const NOTIFICATION_SCREENSHOT_POLL_MS = 250;
export function isPrivateOrInternalHostname(hostname: string): boolean {
const h = hostname.toLowerCase();
const BLOCKED_HOSTS = ["localhost", "metadata.google.internal", "169.254.169.254"];
return (
BLOCKED_HOSTS.includes(h) ||
h.endsWith(".internal") ||
h.startsWith("127.") ||
h === "[::1]" ||
/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/.test(h) ||
h.startsWith("169.254.") ||
h.startsWith("0.")
);
}
function getScreenshotMeta(format: string | undefined): { contentType: string; ext: string } {
const normalized = (format || "jpeg").toLowerCase();
if (normalized === "png") return { contentType: "image/png", ext: "png" };
@@ -175,16 +189,7 @@ async function deliverToUserWebhook(
}
const hostname = parsed.hostname.toLowerCase();
const BLOCKED_HOSTS = ["localhost", "metadata.google.internal", "169.254.169.254"];
if (
BLOCKED_HOSTS.includes(hostname) ||
hostname.endsWith(".internal") ||
hostname.startsWith("127.") ||
hostname === "[::1]" ||
/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/.test(hostname) ||
hostname.startsWith("169.254.") ||
hostname.startsWith("0.")
) {
if (isPrivateOrInternalHostname(hostname)) {
logger.warn(`[notify] blocked webhook to private/internal address: ${hostname}`);
return;
}
@@ -359,6 +364,7 @@ export async function deliverWebPushClientEvent(
canUserAccessClient: (userId: number, userRole: string, clientId: string) => boolean,
getUserRole: (userId: number) => string | undefined,
isClientEventPushEnabled?: (userId: number) => boolean,
isClientOwnedByUser?: (userId: number, clientId: string) => boolean,
): Promise<void> {
const subs = getAllPushSubscriptions();
if (subs.length === 0) return;
@@ -393,7 +399,14 @@ export async function deliverWebPushClientEvent(
if (!role) return;
if (isClientEventPushEnabled && !isClientEventPushEnabled(sub.userId)) return;
if (event === "client_purgatory") {
if (role !== "admin" && role !== "operator") return;
if (role === "admin") {
} else if (role === "operator") {
if (!isClientOwnedByUser || !isClientOwnedByUser(sub.userId, info.id)) {
return;
}
} else {
return;
}
} else if (role !== "admin") {
if (!canUserAccessClient(sub.userId, role, info.id)) return;
}
@@ -16,6 +16,10 @@ export async function handleChatRoutes(
});
}
if (!hasPermission(user.role, "chat:write", user.userId)) {
return new Response("Forbidden: Chat access denied", { status: 403 });
}
const beforeParam = url.searchParams.get("before");
const limitParam = url.searchParams.get("limit");
const before = beforeParam ? Number(beforeParam) : undefined;
@@ -158,11 +158,8 @@ export async function handleClientRoutes(
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try {
requirePermission(user, "clients:control");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
if (req.method === "GET") {
@@ -346,11 +346,8 @@ export async function handleEnrollmentRoutes(
if (req.method === "GET" && url.pathname === "/api/enrollment/banned-ips") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try {
requirePermission(user, "clients:enroll");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
return Response.json({ items: listBannedIps() });
@@ -360,11 +357,8 @@ export async function handleEnrollmentRoutes(
if (req.method === "DELETE" && url.pathname === "/api/enrollment/banned-ips") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try {
requirePermission(user, "clients:enroll");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
const ipToUnban = (url.searchParams.get("ip") || "").trim();
@@ -390,11 +384,8 @@ export async function handleEnrollmentRoutes(
if (req.method === "POST" && url.pathname === "/api/enrollment/ban-ip") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try {
requirePermission(user, "clients:enroll");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
let body: any;
@@ -2,15 +2,17 @@ import { authenticateRequest } from "../../auth";
import { AuditAction, logAudit } from "../../auditLog";
import * as clientManager from "../../clientManager";
import { getConfig, updateNotificationsConfig } from "../../config";
import { savePushSubscription, deletePushSubscription, getPushSubscriptionsByUser } from "../../db";
import { savePushSubscription, deletePushSubscriptionForUser, getPushSubscriptionsByUser } from "../../db";
import { encodeMessage } from "../../protocol";
import {
canUserAccessClient,
getUserNotificationSettings,
updateUserNotificationSettings,
} from "../../users";
import {
DEFAULT_WEBHOOK_TEMPLATE,
DEFAULT_TELEGRAM_TEMPLATE,
isPrivateOrInternalHostname,
renderNotificationTemplate,
} from "../notification-delivery";
import { getVapidPublicKey } from "../web-push";
@@ -20,7 +22,7 @@ type RequestIpProvider = {
};
type NotificationsRouteDeps = {
getNotificationScreenshot: (notificationId: string) => { format?: string; bytes: Uint8Array } | null;
getNotificationScreenshot: (notificationId: string) => { format?: string; bytes: Uint8Array; clientId?: string } | null;
secureHeaders: (contentType?: string) => HeadersInit;
};
@@ -45,6 +47,13 @@ export async function handleNotificationsConfigRoutes(
return new Response("Not found", { status: 404 });
}
if (
screenshot.clientId &&
!canUserAccessClient(user.userId, user.role, screenshot.clientId)
) {
return new Response("Not found", { status: 404 });
}
const format = (screenshot.format || "jpeg").toLowerCase();
const contentType = format === "jpg" || format === "jpeg"
? "image/jpeg"
@@ -67,8 +76,8 @@ export async function handleNotificationsConfigRoutes(
headers: { "Content-Type": "application/json" },
});
}
if (user.role === "viewer") {
return new Response("Forbidden: Viewer access denied", { status: 403 });
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
return Response.json({ notifications: getConfig().notifications });
}
@@ -348,6 +357,13 @@ export async function handleNotificationsConfigRoutes(
return Response.json({ error: "Invalid webhook URL" }, { status: 400 });
}
if (isPrivateOrInternalHostname(parsed.hostname.toLowerCase())) {
return Response.json(
{ error: "Webhook URL points at a private/internal address" },
{ status: 400 },
);
}
const sampleRecord = {
id: "preview-notification",
clientId: "abc123def456",
@@ -489,7 +505,7 @@ export async function handleNotificationsConfigRoutes(
return Response.json({ error: "Missing endpoint" }, { status: 400 });
}
deletePushSubscription(endpoint);
deletePushSubscriptionForUser(user.userId, endpoint);
return Response.json({ ok: true });
}
}
@@ -56,6 +56,9 @@ export async function handleSavedScriptsRoutes(
: "powershell";
const item = saveSavedScript(id, user.userId, name, content, scriptType);
if (!item) {
return Response.json({ error: "Script id already in use" }, { status: 409 });
}
return Response.json({ ok: true, item });
}
@@ -1,7 +1,7 @@
import { authenticateRequest } from "../../auth";
import { logger } from "../../logger";
import { isIpBanned } from "../../db";
import { canUserAccessClient, canUserAccessFeature } from "../../users";
import { canUserAccessClient, canUserAccessFeature, hasPermission } from "../../users";
import type { FeatureName } from "../../users";
type RequestServer = {
@@ -296,6 +296,9 @@ export async function handleWsUpgradeRoutes(
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (!hasPermission(user.role, "chat:write", user.userId)) {
return new Response("Forbidden: Chat access denied", { status: 403 });
}
const ip = server.requestIP(req)?.address || "";
if (server.upgrade(req, { data: { role: "chat_viewer", clientId: "", ip, userRole: user.role, userId: user.userId } })) {
return new Response();
@@ -65,6 +65,7 @@ type CreateDeps = {
pluginState: { enabled: Record<string, boolean>; lastError: Record<string, string> };
getNotificationConfig: () => NotificationConfigShape;
canUserAccessClient: (userId: number, userRole: string, clientId: string) => boolean;
isClientOwnedByUser: (userId: number, clientId: string) => boolean;
getUserRole: (userId: number) => string | undefined;
isClientNotificationsMuted: (clientId: string) => boolean;
storeNotificationScreenshot: (
@@ -459,7 +460,14 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
const sRole = session.userRole ?? session.viewer.data.userRole ?? "";
const sUserId = session.userId ?? session.viewer.data.userId;
if (event === "client_purgatory") {
if (sRole !== "admin" && sRole !== "operator") continue;
if (sRole === "admin") {
} else if (sRole === "operator") {
if (sUserId === undefined || !deps.isClientOwnedByUser(sUserId, info.id)) {
continue;
}
} else {
continue;
}
} else {
if (sRole !== "admin") {
if (sUserId === undefined || !deps.canUserAccessClient(sUserId, sRole, info.id)) {
@@ -473,10 +481,17 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
const externalTargets = deps.getDeliveryTargetsForClientEvent(event, info.id);
const pushEnabledByUser = new Map(externalTargets.map((t) => [t.userId, t.clientEventPush]));
deliverWebPushClientEvent(event, info, deps.canUserAccessClient, deps.getUserRole, (userId) => {
const enabled = pushEnabledByUser.get(userId);
return enabled !== undefined ? enabled : true;
}).catch((err) => logger.warn("[notify] web push client event delivery failed", err));
deliverWebPushClientEvent(
event,
info,
deps.canUserAccessClient,
deps.getUserRole,
(userId) => {
const enabled = pushEnabledByUser.get(userId);
return enabled !== undefined ? enabled : true;
},
deps.isClientOwnedByUser,
).catch((err) => logger.warn("[notify] web push client event delivery failed", err));
deliverClientEventToExternalChannels(event, info, externalTargets).catch((err) =>
logger.warn("[notify] external channel client event delivery failed", err),