fix: update folder name validation to block invalid characters and use shared name schema

This commit is contained in:
Catalin Pit
2026-06-12 15:38:01 +03:00
parent b29da84678
commit 1455a7806a
5 changed files with 64 additions and 13 deletions
@@ -1,4 +1,4 @@
import { ZFolderNameSchema } from '@documenso/lib/utils/folder-name';
import { ZNameSchema } from '@documenso/lib/constants/auth';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
@@ -24,7 +24,7 @@ import { useParams } from 'react-router';
import { z } from 'zod';
const ZCreateFolderFormSchema = z.object({
name: ZFolderNameSchema,
name: ZNameSchema,
});
type TCreateFolderFormSchema = z.infer<typeof ZCreateFolderFormSchema>;
@@ -66,7 +66,7 @@ export const FolderCreateDialog = ({ type, trigger, parentFolderId, ...props }:
toast({
description: t`Folder created successfully`,
});
} catch (err) {
} catch (_err) {
toast({
title: t`Failed to create folder`,
description: t`An unknown error occurred while creating the folder.`,
@@ -1,6 +1,6 @@
import { ZNameSchema } from '@documenso/lib/constants/auth';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { ZFolderNameSchema } from '@documenso/lib/utils/folder-name';
import { trpc } from '@documenso/trpc/react';
import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
import { Button } from '@documenso/ui/primitives/button';
@@ -24,8 +24,6 @@ import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useOptionalCurrentTeam } from '~/providers/team';
export type FolderUpdateDialogProps = {
folder: TFolderWithSubfolders | null;
isOpen: boolean;
@@ -33,7 +31,7 @@ export type FolderUpdateDialogProps = {
} & Omit<DialogPrimitive.DialogProps, 'children'>;
export const ZUpdateFolderFormSchema = z.object({
name: ZFolderNameSchema,
name: ZNameSchema,
visibility: z.nativeEnum(DocumentVisibility).optional(),
});
@@ -41,7 +39,6 @@ export type TUpdateFolderFormSchema = z.infer<typeof ZUpdateFolderFormSchema>;
export const FolderUpdateDialog = ({ folder, isOpen, onOpenChange }: FolderUpdateDialogProps) => {
const { t } = useLingui();
const team = useOptionalCurrentTeam();
const { toast } = useToast();
const { mutateAsync: updateFolder } = trpc.folder.updateFolder.useMutation();
+8 -2
View File
@@ -2,6 +2,7 @@ import MailChecker from 'mailchecker';
import { z } from 'zod';
import { env } from '../utils/env';
import { hasInvalidTextCharacters } from '../utils/zod';
import { NEXT_PUBLIC_WEBAPP_URL } from './app';
export const SALT_ROUNDS = 12;
@@ -9,15 +10,20 @@ export const SALT_ROUNDS = 12;
export const URL_PATTERN = /https?:\/\/|www\./i;
/**
* Shared name schema that disallows URLs to prevent phishing via email rendering.
* Shared name schema that disallows URLs to prevent phishing via email rendering,
* and invisible/control characters that render as empty or break the UI.
* Also disallows invalid resource name characters.
*/
export const ZNameSchema = z
.string()
.trim()
.min(3, { message: 'Please enter a valid name.' })
.min(2, { message: 'Please enter a valid name.' })
.max(255, { message: 'Name cannot be more than 255 characters.' })
.refine((value) => !URL_PATTERN.test(value), {
message: 'Name cannot contain URLs.',
})
.refine((value) => !hasInvalidTextCharacters(value), {
message: 'Name contains invalid characters.',
});
export const IDENTITY_PROVIDER_NAME: Record<string, string> = {
+48
View File
@@ -13,6 +13,54 @@ const EMAIL_REGEX =
const DEFAULT_EMAIL_MESSAGE = 'Invalid email address';
/**
* Code point ranges for control and invisible/formatting characters that render
* as empty or break the UI (e.g. NUL, zero-width spaces, bidi overrides, BOM).
*/
const INVALID_TEXT_CODE_POINT_RANGES: [number, number][] = [
[0x0000, 0x001f],
[0x007f, 0x009f],
[0x00ad, 0x00ad],
[0x034f, 0x034f],
[0x061c, 0x061c],
[0x180e, 0x180e],
[0x200b, 0x200f],
[0x2028, 0x202e],
[0x2060, 0x206f],
[0xfeff, 0xfeff],
[0xd800, 0xdfff],
];
/**
* The same characters expressed as literal "\\uXXXX" text, which can be stored
* verbatim (e.g. the 6 characters `\`, `u`, `0`, `0`, `0`, `0`) and still break
* rendering downstream. This regex is pure ASCII so it is safe to inline.
*/
const INVALID_TEXT_ESCAPE_SEQUENCE_REGEX =
/\\u(?:00[0-1][0-9a-f]|007f|00[89][0-9a-f]|00ad|034f|061c|180e|200[b-f]|202[8-e]|206[0-9a-f]|feff)/iu;
export const hasInvalidTextCharacters = (value: string) => {
if (INVALID_TEXT_ESCAPE_SEQUENCE_REGEX.test(value)) {
return true;
}
for (const char of value) {
const codePoint = char.codePointAt(0);
if (codePoint === undefined) {
continue;
}
const isInvalid = INVALID_TEXT_CODE_POINT_RANGES.some(([start, end]) => codePoint >= start && codePoint <= end);
if (isInvalid) {
return true;
}
}
return false;
};
/**
* Creates a Zod email schema using an RFC 5322 compliant regex.
*
+3 -3
View File
@@ -1,6 +1,6 @@
import { ZNameSchema } from '@documenso/lib/constants/auth';
import { ZFolderTypeSchema } from '@documenso/lib/types/folder-type';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { ZFolderNameSchema } from '@documenso/lib/utils/folder-name';
import { DocumentVisibility } from '@documenso/prisma/generated/types';
import FolderSchema from '@documenso/prisma/generated/zod/modelSchema/FolderSchema';
import { z } from 'zod';
@@ -43,7 +43,7 @@ const ZFolderParentIdSchema = z
.describe('The folder ID to place this folder within. Leave empty to place folder at the root level.');
export const ZCreateFolderRequestSchema = z.object({
name: ZFolderNameSchema,
name: ZNameSchema,
parentId: ZFolderParentIdSchema.optional(),
type: ZFolderTypeSchema.optional(),
});
@@ -53,7 +53,7 @@ export const ZCreateFolderResponseSchema = ZFolderSchema;
export const ZUpdateFolderRequestSchema = z.object({
folderId: z.string().describe('The ID of the folder to update'),
data: z.object({
name: ZFolderNameSchema.optional().describe('The name of the folder'),
name: ZNameSchema.optional().describe('The name of the folder'),
parentId: ZFolderParentIdSchema.optional().nullable(),
visibility: z.nativeEnum(DocumentVisibility).optional().describe('The visibility of the folder'),
pinned: z.boolean().optional().describe('Whether the folder should be pinned'),