fix: issues found in security audit (#1668)

This commit is contained in:
Meier Lukas
2024-12-15 21:16:42 +01:00
committed by GitHub
parent 032509e462
commit 922101dcbd
15 changed files with 70 additions and 27 deletions

View File

@@ -66,9 +66,13 @@ const getBoardAndPermissionsAsync = async (params: Props["params"]) => {
export default async function BoardSettingsPage({ params, searchParams }: Props) { export default async function BoardSettingsPage({ params, searchParams }: Props) {
const { board, permissions } = await getBoardAndPermissionsAsync(params); const { board, permissions } = await getBoardAndPermissionsAsync(params);
const boardSettings = await getServerSettingByKeyAsync(db, "board"); const boardSettings = await getServerSettingByKeyAsync(db, "board");
const { hasFullAccess } = await getBoardPermissionsAsync(board); const { hasFullAccess, hasChangeAccess } = await getBoardPermissionsAsync(board);
const t = await getScopedI18n("board.setting"); const t = await getScopedI18n("board.setting");
if (!hasChangeAccess) {
notFound();
}
return ( return (
<Container> <Container>
<Stack> <Stack>

View File

@@ -1,6 +1,8 @@
import { notFound } from "next/navigation";
import { Stack, Title } from "@mantine/core"; import { Stack, Title } from "@mantine/core";
import { api } from "@homarr/api/server"; import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { getScopedI18n } from "@homarr/translation/server"; import { getScopedI18n } from "@homarr/translation/server";
import { CrawlingAndIndexingSettings } from "~/app/[locale]/manage/settings/_components/crawling-and-indexing.settings"; import { CrawlingAndIndexingSettings } from "~/app/[locale]/manage/settings/_components/crawling-and-indexing.settings";
@@ -20,6 +22,12 @@ export async function generateMetadata() {
} }
export default async function SettingsPage() { export default async function SettingsPage() {
const session = await auth();
if (!session?.user.permissions.includes("admin")) {
notFound();
}
const serverSettings = await api.serverSettings.getAll(); const serverSettings = await api.serverSettings.getAll();
const tSettings = await getScopedI18n("management.page.settings"); const tSettings = await getScopedI18n("management.page.settings");
return ( return (

View File

@@ -1,10 +1,12 @@
import Link from "next/link"; import Link from "next/link";
import { notFound } from "next/navigation";
import { Alert, Anchor, Center, Group, Stack, Table, TableTbody, TableTd, TableTr, Text, Title } from "@mantine/core"; import { Alert, Anchor, Center, Group, Stack, Table, TableTbody, TableTd, TableTr, Text, Title } from "@mantine/core";
import { IconExclamationCircle } from "@tabler/icons-react"; import { IconExclamationCircle } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server"; import { api } from "@homarr/api/server";
import { env } from "@homarr/auth/env.mjs"; import { env } from "@homarr/auth/env.mjs";
import { auth } from "@homarr/auth/next";
import { isProviderEnabled } from "@homarr/auth/server"; import { isProviderEnabled } from "@homarr/auth/server";
import { everyoneGroup } from "@homarr/definitions"; import { everyoneGroup } from "@homarr/definitions";
import { getI18n, getScopedI18n } from "@homarr/translation/server"; import { getI18n, getScopedI18n } from "@homarr/translation/server";
@@ -24,6 +26,12 @@ interface GroupsDetailPageProps {
} }
export default async function GroupsDetailPage({ params, searchParams }: GroupsDetailPageProps) { export default async function GroupsDetailPage({ params, searchParams }: GroupsDetailPageProps) {
const session = await auth();
if (!session?.user.permissions.includes("admin")) {
notFound();
}
const t = await getI18n(); const t = await getI18n();
const tMembers = await getScopedI18n("management.page.group.setting.members"); const tMembers = await getScopedI18n("management.page.group.setting.members");
const group = await api.group.getById({ id: params.id }); const group = await api.group.getById({ id: params.id });

View File

@@ -1,6 +1,8 @@
import { notFound } from "next/navigation";
import { Card, Group, Stack, Text, Title } from "@mantine/core"; import { Card, Group, Stack, Text, Title } from "@mantine/core";
import { api } from "@homarr/api/server"; import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { everyoneGroup } from "@homarr/definitions"; import { everyoneGroup } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server"; import { getScopedI18n } from "@homarr/translation/server";
import { UserAvatar } from "@homarr/ui"; import { UserAvatar } from "@homarr/ui";
@@ -18,6 +20,12 @@ interface GroupsDetailPageProps {
} }
export default async function GroupsDetailPage({ params }: GroupsDetailPageProps) { export default async function GroupsDetailPage({ params }: GroupsDetailPageProps) {
const session = await auth();
if (!session?.user.permissions.includes("admin")) {
notFound();
}
const group = await api.group.getById({ id: params.id }); const group = await api.group.getById({ id: params.id });
const tGeneral = await getScopedI18n("management.page.group.setting.general"); const tGeneral = await getScopedI18n("management.page.group.setting.general");
const tGroupAction = await getScopedI18n("group.action"); const tGroupAction = await getScopedI18n("group.action");

View File

@@ -1,7 +1,9 @@
import React from "react"; import React from "react";
import { notFound } from "next/navigation";
import { Card, CardSection, Divider, Group, Stack, Text, Title } from "@mantine/core"; import { Card, CardSection, Divider, Group, Stack, Text, Title } from "@mantine/core";
import { api } from "@homarr/api/server"; import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { objectKeys } from "@homarr/common"; import { objectKeys } from "@homarr/common";
import type { GroupPermissionKey } from "@homarr/definitions"; import type { GroupPermissionKey } from "@homarr/definitions";
import { groupPermissions } from "@homarr/definitions"; import { groupPermissions } from "@homarr/definitions";
@@ -16,6 +18,12 @@ interface GroupPermissionsPageProps {
} }
export default async function GroupPermissionsPage({ params }: GroupPermissionsPageProps) { export default async function GroupPermissionsPage({ params }: GroupPermissionsPageProps) {
const session = await auth();
if (!session?.user.permissions.includes("admin")) {
notFound();
}
const group = await api.group.getById({ id: params.id }); const group = await api.group.getById({ id: params.id });
const tPermissions = await getScopedI18n("group.permission"); const tPermissions = await getScopedI18n("group.permission");
const t = await getI18n(); const t = await getI18n();

View File

@@ -575,11 +575,14 @@ export const boardRouter = createTRPCRouter({
); );
}); });
}), }),
importOldmarrConfig: protectedProcedure.input(importJsonFileSchema).mutation(async ({ input, ctx }) => { importOldmarrConfig: permissionRequiredProcedure
const content = await input.file.text(); .requiresPermission("board-create")
const oldmarr = oldmarrConfigSchema.parse(JSON.parse(content)); .input(importJsonFileSchema)
await importOldmarrAsync(ctx.db, oldmarr, input.configuration); .mutation(async ({ input, ctx }) => {
}), const content = await input.file.text();
const oldmarr = oldmarrConfigSchema.parse(JSON.parse(content));
await importOldmarrAsync(ctx.db, oldmarr, input.configuration);
}),
}); });
const noBoardWithSimilarNameAsync = async (db: Database, name: string, ignoredIds: string[] = []) => { const noBoardWithSimilarNameAsync = async (db: Database, name: string, ignoredIds: string[] = []) => {

View File

@@ -6,11 +6,12 @@ import { invites } from "@homarr/db/schema/sqlite";
import { selectInviteSchema } from "@homarr/db/validationSchemas"; import { selectInviteSchema } from "@homarr/db/validationSchemas";
import { z } from "@homarr/validation"; import { z } from "@homarr/validation";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
import { throwIfCredentialsDisabled } from "./invite/checks"; import { throwIfCredentialsDisabled } from "./invite/checks";
export const inviteRouter = createTRPCRouter({ export const inviteRouter = createTRPCRouter({
getAll: protectedProcedure getAll: permissionRequiredProcedure
.requiresPermission("admin")
.output( .output(
z.array( z.array(
selectInviteSchema selectInviteSchema
@@ -40,7 +41,8 @@ export const inviteRouter = createTRPCRouter({
}, },
}); });
}), }),
createInvite: protectedProcedure createInvite: permissionRequiredProcedure
.requiresPermission("admin")
.input( .input(
z.object({ z.object({
expirationDate: z.date(), expirationDate: z.date(),
@@ -65,7 +67,8 @@ export const inviteRouter = createTRPCRouter({
token, token,
}; };
}), }),
deleteInvite: protectedProcedure deleteInvite: permissionRequiredProcedure
.requiresPermission("admin")
.input( .input(
z.object({ z.object({
id: z.string(), id: z.string(),

View File

@@ -3,17 +3,18 @@ import type { ServerSettings } from "@homarr/server-settings";
import { defaultServerSettingsKeys } from "@homarr/server-settings"; import { defaultServerSettingsKeys } from "@homarr/server-settings";
import { validation, z } from "@homarr/validation"; import { validation, z } from "@homarr/validation";
import { createTRPCRouter, onboardingProcedure, protectedProcedure, publicProcedure } from "../trpc"; import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, publicProcedure } from "../trpc";
import { nextOnboardingStepAsync } from "./onboard/onboard-queries"; import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
export const serverSettingsRouter = createTRPCRouter({ export const serverSettingsRouter = createTRPCRouter({
getCulture: publicProcedure.query(async ({ ctx }) => { getCulture: publicProcedure.query(async ({ ctx }) => {
return await getServerSettingByKeyAsync(ctx.db, "culture"); return await getServerSettingByKeyAsync(ctx.db, "culture");
}), }),
getAll: protectedProcedure.query(async ({ ctx }) => { getAll: permissionRequiredProcedure.requiresPermission("admin").query(async ({ ctx }) => {
return await getServerSettingsAsync(ctx.db); return await getServerSettingsAsync(ctx.db);
}), }),
saveSettings: protectedProcedure saveSettings: permissionRequiredProcedure
.requiresPermission("admin")
.input( .input(
z.object({ z.object({
settingsKey: z.enum(defaultServerSettingsKeys), settingsKey: z.enum(defaultServerSettingsKeys),

View File

@@ -11,7 +11,7 @@ import { inviteRouter } from "../invite";
const defaultSession = { const defaultSession = {
user: { user: {
id: createId(), id: createId(),
permissions: [], permissions: ["admin"],
colorScheme: "light", colorScheme: "light",
}, },
expires: new Date().toISOString(), expires: new Date().toISOString(),

View File

@@ -15,7 +15,7 @@ vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
const defaultSession = { const defaultSession = {
user: { user: {
id: createId(), id: createId(),
permissions: [], permissions: ["admin"],
colorScheme: "light", colorScheme: "light",
}, },
expires: new Date().toISOString(), expires: new Date().toISOString(),

View File

@@ -1,9 +1,9 @@
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker"; import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
export const updateCheckerRouter = createTRPCRouter({ export const updateCheckerRouter = createTRPCRouter({
getAvailableUpdates: protectedProcedure.query(async () => { getAvailableUpdates: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
const handler = updateCheckerRequestHandler.handler({}); const handler = updateCheckerRequestHandler.handler({});
const data = await handler.getCachedOrUpdatedDataAsync({}); const data = await handler.getCachedOrUpdatedDataAsync({});
return data.data.availableUpdates; return data.data.availableUpdates;

View File

@@ -10,7 +10,7 @@ import { controlsInputSchema } from "@homarr/integrations/types";
import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole"; import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole";
import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
export const dnsHoleRouter = createTRPCRouter({ export const dnsHoleRouter = createTRPCRouter({
summary: publicProcedure summary: publicProcedure
@@ -62,7 +62,7 @@ export const dnsHoleRouter = createTRPCRouter({
}); });
}), }),
enable: publicProcedure enable: protectedProcedure
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole"))) .unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
.mutation(async ({ ctx: { integration } }) => { .mutation(async ({ ctx: { integration } }) => {
const client = integrationCreator(integration); const client = integrationCreator(integration);
@@ -75,7 +75,7 @@ export const dnsHoleRouter = createTRPCRouter({
}); });
}), }),
disable: publicProcedure disable: protectedProcedure
.input(controlsInputSchema) .input(controlsInputSchema)
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole"))) .unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
.mutation(async ({ ctx: { integration }, input }) => { .mutation(async ({ ctx: { integration }, input }) => {

View File

@@ -9,7 +9,7 @@ import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-ma
import type { IntegrationAction } from "../../middlewares/integration"; import type { IntegrationAction } from "../../middlewares/integration";
import { createManyIntegrationMiddleware } from "../../middlewares/integration"; import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
const createIndexerManagerIntegrationMiddleware = (action: IntegrationAction) => const createIndexerManagerIntegrationMiddleware = (action: IntegrationAction) =>
createManyIntegrationMiddleware(action, ...getIntegrationKindsByCategory("indexerManager")); createManyIntegrationMiddleware(action, ...getIntegrationKindsByCategory("indexerManager"));
@@ -54,7 +54,7 @@ export const indexerManagerRouter = createTRPCRouter({
}; };
}); });
}), }),
testAllIndexers: publicProcedure testAllIndexers: protectedProcedure
.unstable_concat(createIndexerManagerIntegrationMiddleware("interact")) .unstable_concat(createIndexerManagerIntegrationMiddleware("interact"))
.mutation(async ({ ctx }) => { .mutation(async ({ ctx }) => {
await Promise.all( await Promise.all(

View File

@@ -5,10 +5,10 @@ import { eq } from "@homarr/db";
import { items } from "@homarr/db/schema/sqlite"; import { items } from "@homarr/db/schema/sqlite";
import { z } from "@homarr/validation"; import { z } from "@homarr/validation";
import { createTRPCRouter, publicProcedure } from "../../trpc"; import { createTRPCRouter, protectedProcedure } from "../../trpc";
export const notebookRouter = createTRPCRouter({ export const notebookRouter = createTRPCRouter({
updateContent: publicProcedure updateContent: protectedProcedure
.input( .input(
z.object({ z.object({
itemId: z.string(), itemId: z.string(),

View File

@@ -7,7 +7,7 @@ import { z } from "@homarr/validation";
import type { IntegrationAction } from "../../middlewares/integration"; import type { IntegrationAction } from "../../middlewares/integration";
import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
const createSmartHomeIntegrationMiddleware = (action: IntegrationAction) => const createSmartHomeIntegrationMiddleware = (action: IntegrationAction) =>
createOneIntegrationMiddleware(action, ...getIntegrationKindsByCategory("smartHomeServer")); createOneIntegrationMiddleware(action, ...getIntegrationKindsByCategory("smartHomeServer"));
@@ -41,7 +41,7 @@ export const smartHomeRouter = createTRPCRouter({
}; };
}); });
}), }),
switchEntity: publicProcedure switchEntity: protectedProcedure
.unstable_concat(createSmartHomeIntegrationMiddleware("interact")) .unstable_concat(createSmartHomeIntegrationMiddleware("interact"))
.input(z.object({ entityId: z.string() })) .input(z.object({ entityId: z.string() }))
.mutation(async ({ ctx: { integration }, input }) => { .mutation(async ({ ctx: { integration }, input }) => {
@@ -53,7 +53,7 @@ export const smartHomeRouter = createTRPCRouter({
return success; return success;
}), }),
executeAutomation: publicProcedure executeAutomation: protectedProcedure
.unstable_concat(createSmartHomeIntegrationMiddleware("interact")) .unstable_concat(createSmartHomeIntegrationMiddleware("interact"))
.input(z.object({ automationId: z.string() })) .input(z.object({ automationId: z.string() }))
.mutation(async ({ ctx: { integration }, input }) => { .mutation(async ({ ctx: { integration }, input }) => {