Fix decryptText crash on pre-encryption secret application variables

https://sonarly.com/issue/26650?type=bug

The `decryptText` function in `auth.util.ts` crashes with "Invalid initialization vector" when attempting to decrypt an application variable value that was stored as plaintext (not encrypted), because the base64-decoded plaintext is shorter than the required 16-byte IV for AES-256-CTR.

Fix: Added input validation to `SecretEncryptionService` to prevent crashes when attempting to decrypt values that are not valid encrypted payloads (e.g., plaintext values stored before encryption was introduced in commit 7e3d9cd85a).

**Changes to `secret-encryption.service.ts`:**

1. Added `isValidEncryptedValue()` private method that checks if a base64-decoded value is at least 17 bytes (16-byte IV + minimum 1 byte ciphertext), which is the minimum for a valid AES-256-CTR encrypted payload.

2. In `decrypt()`: Before calling `decryptText`, validates the value. If invalid, logs a warning and returns the raw value instead of crashing. This protects callers like `build-env-var.ts` and `config-storage.service.ts`.

3. In `decryptAndMask()`: Before attempting decryption, validates the value. If invalid, logs a warning and returns just the mask string. This is the correct UX for the application settings page — showing `******` instead of crashing the entire FindOneApplication query.

The fix handles the root cause scenario where secret application variables were stored as plaintext before the encryption feature was deployed, and are now being read by the post-encryption code.
This commit is contained in:
Sonarly Claude Code 2026-04-15 15:05:53 +00:00
parent 7ba5fe32f8
commit da86e5a147

View file

@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
@ -8,8 +8,14 @@ import {
} from 'src/engine/core-modules/auth/auth.util';
import { EnvironmentConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/environment-config.driver';
// AES-256-CTR requires a 16-byte IV prepended to the ciphertext.
// A valid encrypted payload must be at least 17 bytes (16 IV + 1 ciphertext).
const MIN_ENCRYPTED_PAYLOAD_BYTES = 17;
@Injectable()
export class SecretEncryptionService {
private readonly logger = new Logger(SecretEncryptionService.name);
constructor(
private readonly environmentConfigDriver: EnvironmentConfigDriver,
) {}
@ -18,6 +24,12 @@ export class SecretEncryptionService {
return this.environmentConfigDriver.get('APP_SECRET');
}
private isValidEncryptedValue(value: string): boolean {
const buffer = Buffer.from(value, 'base64');
return buffer.length >= MIN_ENCRYPTED_PAYLOAD_BYTES;
}
public encrypt(value: string): string {
if (!isDefined(value)) {
return value;
@ -33,6 +45,14 @@ export class SecretEncryptionService {
return value;
}
if (!this.isValidEncryptedValue(value)) {
this.logger.warn(
'Attempted to decrypt a value that is not a valid encrypted payload — returning raw value',
);
return value;
}
const appSecret = this.getAppSecret();
return decryptText(value, appSecret);
@ -49,6 +69,14 @@ export class SecretEncryptionService {
return value;
}
if (!this.isValidEncryptedValue(value)) {
this.logger.warn(
'Attempted to decrypt-and-mask a value that is not a valid encrypted payload — returning mask',
);
return mask;
}
const decryptedValue = this.decrypt(value);
const visibleCharsCount = Math.min(