mirror of
https://github.com/documenso/documenso.git
synced 2026-07-14 06:47:10 +10:00
fix: update folder name validation to block invalid characters and use shared name schema
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user