Prevent admins from entering malicious javascript into link settings

This commit is contained in:
Karl Ludwig Weise
2026-05-05 11:31:36 +02:00
parent 5f61a2116d
commit 98a107d4b4
4 changed files with 47 additions and 5 deletions
+20 -2
View File
@@ -43,6 +43,10 @@
</script>
<script lang="ts">
/**
Custom changes:
* Added derivedHref to prevent malicious js injection via href prop.
*/
let {
class: className,
variant = "default",
@@ -55,15 +59,29 @@
isLoading,
...restProps
}: ButtonProps = $props();
let derivedHref = $derived.by(() => {
if (
href &&
(href.startsWith("http://") ||
href.startsWith("https://") ||
href.startsWith("mailto:") ||
href.startsWith("tel:") ||
href.startsWith("/"))
) {
return href;
}
return undefined;
});
</script>
{#if href}
{#if derivedHref}
<!-- eslint-disable svelte/no-navigation-without-resolve -->
<a
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href}
href={disabled ? undefined : derivedHref}
aria-disabled={disabled}
role={disabled ? "link" : undefined}
tabindex={disabled ? -1 : undefined}
@@ -9,7 +9,7 @@ import logger from "$lib/logger";
import { and, count, eq, ne, not, or } from "drizzle-orm";
import { z } from "zod";
import { ConflictError, NotFoundError, ValidationError } from "../utils/errors";
import { redactDbUrl } from "../utils/url";
import { isLinkValid, redactDbUrl } from "../utils/url";
if (!process.env.BUILDING && !env.DATABASE_URL) {
throw new Error("DATABASE_URL is not set");
@@ -223,7 +223,7 @@ export class TenantAdminService {
updateData: Partial<
Pick<
InsertTenant,
"longName" | "shortName" | "descriptions" | "languages" | "logo" | "domain"
"longName" | "shortName" | "descriptions" | "languages" | "logo" | "domain" | "links"
>
>,
) {
@@ -237,6 +237,15 @@ export class TenantAdminService {
throw new ValidationError("Shortname cannot be changed");
}
// Check each link
if (updateData.links) {
for (const link of Object.values(updateData.links)) {
if (!isLinkValid(link)) {
throw new ValidationError("Links must start with http or https or be empty");
}
}
}
// Check if domain is already in use
if (updateData.domain) {
const domainExists = await centralDb
+6
View File
@@ -4,3 +4,9 @@ export const redactDbUrl = (input: string) => {
url.password = "redacted-pw";
return url.toString();
};
export const isLinkValid = (link: string | undefined) => {
return (
link?.startsWith("http://") || link?.startsWith("https://") || link === "" || link === undefined
);
};
@@ -2,7 +2,16 @@ import { m } from "$i18n/messages";
import { z } from "zod";
const optionalUrl = (errorMessage: string) =>
z.union([z.literal(""), z.url({ message: errorMessage })]).optional();
z
.union([
z.literal(""),
z
.url({ message: errorMessage })
.refine((url) => url.startsWith("http://") || url.startsWith("https://"), {
message: errorMessage,
}),
])
.optional();
export const formSchema = z
.object({