From 0b3dde17977f73b3587b93444ea1be8636f700e7 Mon Sep 17 00:00:00 2001 From: Tor Valstrom <51440386+torvalstrom@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:13:53 +0000 Subject: [PATCH] fix(files): honour the escape flag in every filename validity message getFilenameValidity() documents its second parameter as "Escape the matched string in the error (only set when used in HTML)", but only the forbidden-character branch passed it on. The reserved-name and forbidden-extension branches hardcoded `escape: false`, so a caller that asked for escaping because it renders the result as HTML got the matched segment back raw. The segments those two branches report come from server configuration - forbidden_filenames and forbidden_filename_extensions - so this is not reachable by an unprivileged user, and none of the three call sites in the tree ask for escaping today. It is the parameter quietly not doing what it says that is worth fixing, before a caller relies on it. Note the forbidden-character check runs first, which is why the existing escape tests could only ever cover that branch: any name containing <, > or " is rejected there. The new tests use & instead, which is escaped by the translation layer but is not a forbidden filename character, so it reaches the other two branches. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Tor Valstrom <51440386+torvalstrom@users.noreply.github.com> --- apps/files/src/utils/filenameValidity.spec.ts | 12 ++++++++++-- apps/files/src/utils/filenameValidity.ts | 10 +++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/apps/files/src/utils/filenameValidity.spec.ts b/apps/files/src/utils/filenameValidity.spec.ts index 2687956efeb6c..e17a38a9bbbf8 100644 --- a/apps/files/src/utils/filenameValidity.spec.ts +++ b/apps/files/src/utils/filenameValidity.spec.ts @@ -10,9 +10,9 @@ vi.mock('@nextcloud/capabilities', () => ({ getCapabilities: () => ({ files: { forbidden_filename_characters: ['/', '\\', '>'], - forbidden_filenames: ['.htaccess'], + forbidden_filenames: ['.htaccess', 'a&b'], forbidden_filename_basenames: ['con'], - forbidden_filename_extensions: ['.exe', '.~'], + forbidden_filename_extensions: ['.exe', '.~', '.a&b'], }, }), })) @@ -87,6 +87,14 @@ describe('getFilenameValidity', () => { it('does not escape the matched character by default', () => { expect(getFilenameValidity('inva>lid')).toBe('">" is not allowed inside a filename.') }) + + it('escapes a reserved name when requested', () => { + expect(getFilenameValidity('a&b', true)).toBe('"a&b" is a reserved name and not allowed for filenames.') + }) + + it('escapes a forbidden extension when requested', () => { + expect(getFilenameValidity('file.a&b', true)).toBe('".a&b" is not an allowed filetype.') + }) }) it('rethrows errors that are not InvalidFilenameError', async () => { diff --git a/apps/files/src/utils/filenameValidity.ts b/apps/files/src/utils/filenameValidity.ts index b018352a8b272..d114a35ee24e6 100644 --- a/apps/files/src/utils/filenameValidity.ts +++ b/apps/files/src/utils/filenameValidity.ts @@ -37,17 +37,17 @@ export function getFilenameValidity(name: string, escape = false, isFolder = fal return t('files', '"{char}" is not allowed inside a filename.', { char: error.segment }, { escape }) case InvalidFilenameErrorReason.ReservedName: if (isFolder) { - return t('files', '"{segment}" is a reserved name and not allowed for folder names.', { segment: error.segment }, { escape: false }) + return t('files', '"{segment}" is a reserved name and not allowed for folder names.', { segment: error.segment }, { escape }) } - return t('files', '"{segment}" is a reserved name and not allowed for filenames.', { segment: error.segment }, { escape: false }) + return t('files', '"{segment}" is a reserved name and not allowed for filenames.', { segment: error.segment }, { escape }) case InvalidFilenameErrorReason.Extension: if (!isFolder && error.segment.match(/\.[a-z]/i)) { - return t('files', '"{extension}" is not an allowed filetype.', { extension: error.segment }, { escape: false }) + return t('files', '"{extension}" is not an allowed filetype.', { extension: error.segment }, { escape }) } if (isFolder) { - return t('files', 'Folder names must not end with "{extension}".', { extension: error.segment }, { escape: false }) + return t('files', 'Folder names must not end with "{extension}".', { extension: error.segment }, { escape }) } - return t('files', 'Filenames must not end with "{extension}".', { extension: error.segment }, { escape: false }) + return t('files', 'Filenames must not end with "{extension}".', { extension: error.segment }, { escape }) default: if (isFolder) { return t('files', 'Invalid folder name.')