fix(aliases): verify alias regex in frontend and backend

Co-authored-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson
2026-02-18 22:27:45 +01:00
parent 2a2bd92265
commit 29f60f1d9b
8 changed files with 70 additions and 12 deletions
+10 -3
View File
@@ -282,10 +282,17 @@ describe('AliasService', () => {
await expect(service.ensureAliasIsAvailable(alias1)).rejects.toThrow(AlreadyInDBError);
expectBindings(tracker, 'select', [[alias1.toLowerCase()]]);
});
it('returns void if alias can be used', async () => {
it.each(['note?', 'alias!', 'pizza ', 'te.st', 'test;1'])(
"throws ForbiddenIdError for aliases with invalid alias '%s'",
async (alias) => {
await expect(service.ensureAliasIsAvailable(alias)).rejects.toThrow(ForbiddenIdError);
},
);
it.each([alias1, '🍕', 'pizza', 'Pizza', 'pizzä', 'pißßa'])("accepts '%s'", async (alias) => {
mockSelect(tracker, [FieldNameAlias.alias], TableAlias, FieldNameAlias.alias, []);
await service.ensureAliasIsAvailable(alias1);
expectBindings(tracker, 'select', [[alias1.toLowerCase()]]);
await service.ensureAliasIsAvailable(alias);
expectBindings(tracker, 'select', [[alias.toLowerCase()]]);
});
});
+14 -3
View File
@@ -3,14 +3,16 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Alias, FieldNameAlias, TableAlias } from '@hedgedoc/database';
import type { Alias } from '@hedgedoc/database';
import { FieldNameAlias, TableAlias } from '@hedgedoc/database';
import { Inject, Injectable } from '@nestjs/common';
import base32Encode from 'base32-encode';
import { Knex } from 'knex';
import { InjectConnection } from 'nest-knexjs';
import { randomBytes } from 'node:crypto';
import noteConfiguration, { NoteConfig } from '../config/note.config';
import noteConfiguration from '../config/note.config';
import { NoteConfig } from '../config/note.config';
import { AliasDto } from '../dtos/alias.dto';
import {
AlreadyInDBError,
@@ -21,8 +23,10 @@ import {
} from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { NoteEvent, NoteEventMap } from '../events';
import { NoteEventMap } from '../events';
import { NoteEvent } from '../events';
import type { NoteAliasesDto } from '../dtos/note-aliases.dto';
import { ALIAS_REGEX } from '@hedgedoc/commons';
type AliasAndIsPrimary = Pick<Alias, FieldNameAlias.alias | FieldNameAlias.isPrimary>;
@@ -243,6 +247,13 @@ export class AliasService {
* @throws AlreadyInDBError The requested alias already exists
*/
async ensureAliasIsAvailable(alias: string, transaction?: Knex): Promise<void> {
if (!ALIAS_REGEX.test(alias)) {
throw new ForbiddenIdError(
`The alias '${alias}' contains invalid characters.`,
this.logger.getContext(),
'ensureAliasIsAvailable',
);
}
if (this.isAliasForbidden(alias)) {
throw new ForbiddenIdError(
`The alias '${alias}' is forbidden by the administrator.`,
+35
View File
@@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { describe, expect, it } from '@jest/globals'
import { ALIAS_REGEX } from './alias.js'
describe('alias regex', () => {
describe.each([
'🍕',
'pizza',
'Pizza',
'pizzä',
'pißßa',
'PiZzA🍕',
'pi-zza',
'pi_zza',
'🧝‍♀️',
'⚰️',
'🏳️‍🌈',
'🧑‍🧑‍🧒‍🧒',
'🇩🇪',
])('allows', (testAlias) => {
it(testAlias, () => {
expect(ALIAS_REGEX.test(testAlias)).toBe(true)
})
})
describe.each(['', '\t', ' ', '\u200D', '⌘', '𝑯𝒆𝒍𝒍𝒐 𝑾𝒐𝒓𝒍𝒅'])('forbids', (testAlias) => {
it(testAlias, () => {
expect(ALIAS_REGEX.test(testAlias)).toBe(false)
})
})
})
+6
View File
@@ -0,0 +1,6 @@
/*
* SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const ALIAS_REGEX = /^[\p{Letter}\p{Mark}\p{Number}_\-\p{RGI_Emoji}]+$/v
+1 -1
View File
@@ -3,5 +3,5 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * from './alias.js'
export * from './username.js'
+1 -1
View File
@@ -2,7 +2,7 @@
"extends" : "./tsconfig.base.json",
"compilerOptions": {
"module": "CommonJS",
"target": "ES2015",
"target": "ES2024",
"outDir": "dist/cjs",
"declarationDir": "dist/cjs",
"moduleResolution": "node"
@@ -7,7 +7,7 @@ exports[`AliasesAddForm renders the input form 1`] = `
class="me-1 mb-1 input-group has-validation"
>
<input
class="form-control"
class="form-control is-invalid"
data-testid="addAliasInput"
placeholder="editor.modal.aliases.addAlias"
required=""
@@ -16,8 +16,7 @@ import type { FormEvent } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import { Button, Form, InputGroup } from 'react-bootstrap'
import { PlusLg as IconPlus } from 'react-bootstrap-icons'
const validAliasRegex = /^[a-zA-Z0-9_-]*$/
import { ALIAS_REGEX } from '@hedgedoc/commons'
/**
* Form for adding a new alias to a note.
@@ -47,7 +46,7 @@ export const AliasesAddForm: React.FC = () => {
const onNewAliasInputChange = useOnInputChange(setNewAlias)
const newAliasValid = useMemo(() => {
return validAliasRegex.test(newAlias)
return ALIAS_REGEX.test(newAlias)
}, [newAlias])
const addAliasText = useTranslatedText('editor.modal.aliases.addAlias')