From 6f37b6f970e74b720365c714fbceb7c6337f534f Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Wed, 13 May 2026 17:12:33 +0200 Subject: [PATCH] fix(ipsec): reject PSK curly braces Reject unsupported curly brace characters in imported IPsec pre-shared keys and show a tooltip next to the field so users know which characters are not accepted. Add unit coverage for the new validation helper so this validation rule stays protected. Assisted-by: Copilot:gpt-5.4 --- .../ipsec_tunnel/CreateOrEditTunnelDrawer.vue | 21 +++++++++++------ src/i18n/en.json | 2 ++ src/i18n/it.json | 2 ++ .../__tests__/validateNoCurlyBraces.spec.ts | 23 +++++++++++++++++++ src/lib/validation.ts | 11 +++++++++ 5 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 src/lib/__tests__/validateNoCurlyBraces.spec.ts diff --git a/src/components/standalone/ipsec_tunnel/CreateOrEditTunnelDrawer.vue b/src/components/standalone/ipsec_tunnel/CreateOrEditTunnelDrawer.vue index 45d6e74f9..bc22431ac 100644 --- a/src/components/standalone/ipsec_tunnel/CreateOrEditTunnelDrawer.vue +++ b/src/components/standalone/ipsec_tunnel/CreateOrEditTunnelDrawer.vue @@ -8,6 +8,7 @@ import { MessageBag, validateHost, validateIp4Cidr, + validateNoCurlyBraces, validatePositiveInteger, validateRequired, validateRequiredOption, @@ -355,12 +356,10 @@ function validateFormByStep(step: number): boolean { if (presharedKeyMode.value === 'generate') { return true } else { - const validator = validateRequired(presharedKey.value) - if (!validator.valid) { - validationErrorBag.value.set('presharedKey', [t(validator.errMessage as string)]) - return false - } - return true + return runValidators( + [validateRequired(presharedKey.value), validateNoCurlyBraces(presharedKey.value)], + 'presharedKey' + ) } } else { const step3Validators: [validationOutput[], string][] = [ @@ -611,7 +610,15 @@ watch( v-model="presharedKey" :invalid-message="validationErrorBag.getFirstFor('presharedKey')" :label="id ? t('standalone.ipsec_tunnel.pre_shared_key') : ''" - /> + > + +
diff --git a/src/i18n/en.json b/src/i18n/en.json index 8a07b0f16..0f68f40e5 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -84,6 +84,7 @@ "required": "Required", "unique": "Enter a unique value", "invalid": "Field has incorrect value", + "curly_braces_not_allowed": "Curly brace characters are not allowed", "invalid_format": "File is not a valid PEM format", "expired": "Certificate has expired", "key_mismatch": "Private key does not match the certificate", @@ -2182,6 +2183,7 @@ "choose_wan": "Choose WAN", "add_network": "Add network", "pre_shared_key": "Pre-shared key", + "pre_shared_key_invalid_chars_tooltip": "The characters '\\{' and '\\}' are not allowed in the pre-shared key", "use_generated_key": "Use generated key", "use_custom_key": "Use custom key", "dpd_dead_peer_detection": "DPD (dead peer detection)", diff --git a/src/i18n/it.json b/src/i18n/it.json index 630ce0304..ba07f7ebe 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -82,6 +82,7 @@ "http_404": "Risorsa non trovata", "http_500": "Errore del server", "required": "Obbligatorio", + "curly_braces_not_allowed": "I caratteri di parentesi graffe non sono consentiti", "invalid_hostname": "Nome host non valido", "hostname_is_too_long": "Il nome host ha troppi caratteri", "cannot_save_configuration": "Impossibile salvare la configurazione", @@ -1563,6 +1564,7 @@ "wan_ip_address": "Indirizzo IP WAN", "enable": "Abilita", "pre_shared_key": "Chiave condivisa", + "pre_shared_key_invalid_chars_tooltip": "I caratteri '\\{' e '\\}' non sono consentiti nella chiave condivisa", "remote_ip_address_tooltip": "Inserire l'indirizzo IP pubblico o l'host del server remoto. Se il server remoto ha un indirizzo IP dinamico, รจ sufficiente inserire 'qualsiasi'", "edit_ipsec_tunnel": "Modifica tunnel IPsec", "ike_version": "Versione IKE", diff --git a/src/lib/__tests__/validateNoCurlyBraces.spec.ts b/src/lib/__tests__/validateNoCurlyBraces.spec.ts new file mode 100644 index 000000000..9cda7c350 --- /dev/null +++ b/src/lib/__tests__/validateNoCurlyBraces.spec.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest' +import { validateNoCurlyBraces } from '@/lib/validation' + +describe('validateNoCurlyBraces', () => { + it('accepts an empty string', () => { + expect(validateNoCurlyBraces('').valid).toBe(true) + }) + + it('accepts whitespace-only values', () => { + expect(validateNoCurlyBraces(' ').valid).toBe(true) + }) + + it.each(['{secret', 'secret}', 'sec{ret}', '{}'])('rejects "%s"', (input) => { + const result = validateNoCurlyBraces(input) + + expect(result.valid).toBe(false) + expect(result.errMessage).toBeDefined() + }) + + it('accepts special characters other than curly braces', () => { + expect(validateNoCurlyBraces('p@ssw0rd![]()').valid).toBe(true) + }) +}) diff --git a/src/lib/validation.ts b/src/lib/validation.ts index 9ff202d92..3e5ff5a1c 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -733,6 +733,17 @@ export function validateNoSpaces(value: string): validationOutput { return { valid: true } } +/** + * Validate if the string doesn't have curly braces. + * @param value + */ +export function validateNoCurlyBraces(value: string): validationOutput { + if (value.includes('{') || value.includes('}')) { + return { valid: false, errMessage: 'error.curly_braces_not_allowed' } + } + return { valid: true } +} + /** * Validate a 6-digit code *