-
-
Notifications
You must be signed in to change notification settings - Fork 40
Description
Affected package
prisma-field-encryption
Tested with:
prisma-field-encryption (latest compatible)
Prisma 6.13.0
SQLite backend
Summary
When using the annotation /// @Encrypted?mode=strict, the library correctly throws an error when ciphertext is corrupted and decryption fails. However, if a value stored in the encrypted column does not match the expected ciphertext format, the extension does not attempt decryption and returns the raw value as plaintext.
This creates a strict-mode bypass condition where non-ciphertext values stored in encrypted fields are accepted and returned by the application without error, even though strict mode is enabled.
Impact
Strict mode implies fail-closed behavior. The current behavior is:
Valid ciphertext → decrypted successfully
Corrupted ciphertext → decryption error thrown (expected)
Non-ciphertext value → returned as plaintext (unexpected in strict mode)
If an attacker or system fault can write directly to the database, including through:
SQL injection
raw queries ($executeRaw)
migration errors
backup corruption
replication inconsistencies
insider access
They can inject arbitrary values into encrypted fields and the application will accept them as legitimate decrypted values.
Potential consequences:
Business logic bypass
Integrity violation of protected fields
Unexpected plaintext propagation into logs, APIs, or templates
Application state manipulation
Reproduction steps
Prisma schema
model User {
id Int @id @default(autoincrement())
email String @unique
secret String? /// @Encrypted?mode=strict
}
Normal encryption/decryption flow (expected behavior)
basic.cjs
require("dotenv/config");
const { PrismaClient } = require("@prisma/client");
const { fieldEncryptionExtension } = require("prisma-field-encryption");
console.log("START basic.cjs");
const prisma = new PrismaClient().$extends(fieldEncryptionExtension());
async function main() {
await prisma.user.deleteMany();
const u = await prisma.user.create({
data: { email: "[email protected]", secret: "TOPSECRET" },
});
const read = await prisma.user.findUnique({ where: { id: u.id } });
console.log("READ:", read);
}
main()
.then(() => console.log("DONE basic.cjs"))
.catch(console.error)
.finally(() => prisma.$disconnect());
Output:
START basic.cjs
READ: { id: 4, email: '[email protected]', secret: 'TOPSECRET' }
DONE basic.cjs
Encryption and decryption function correctly.
Corrupting valid ciphertext (strict mode behaves correctly)
strict_probe.cjs
require("dotenv/config");
const { PrismaClient } = require("@prisma/client");
const { fieldEncryptionExtension } = require("prisma-field-encryption");
console.log("START strict_probe.cjs");
const prisma = new PrismaClient().$extends(fieldEncryptionExtension());
async function main() {
await prisma.user.deleteMany();
await prisma.user.create({
data: { email: "[email protected]", secret: "TOPSECRET" },
});
const rows = await prisma.$queryRawUnsafe(
"SELECT secret FROM User WHERE email = '[email protected]'"
);
const raw = rows[0].secret;
console.log("RAW IN DB:", raw);
const corrupted = raw.slice(0, raw.length - 8);
await prisma.$executeRawUnsafe(
"UPDATE User SET secret = ? WHERE email = '[email protected]'",
corrupted
);
await prisma.user.findUnique({ where: { email: "[email protected]" } });
}
main()
.then(() => console.log("DONE"))
.catch((e) => console.error("ERROR strict_probe.cjs:", e))
.finally(() => prisma.$disconnect());
Output:
RAW IN DB: v1.aesgcm256....
ERROR strict_probe.cjs: Error: decryption error(s) encountered...
Strict mode correctly throws when ciphertext is invalid.
Strict-mode bypass via non-ciphertext injection
poison.cjs
require("dotenv/config");
const { PrismaClient } = require("@prisma/client");
const { fieldEncryptionExtension } = require("prisma-field-encryption");
console.log("START poison.cjs");
const prisma = new PrismaClient().$extends(fieldEncryptionExtension());
async function main() {
await prisma.user.upsert({
where: { email: "[email protected]" },
update: { secret: "TOPSECRET" },
create: { email: "[email protected]", secret: "TOPSECRET" },
});
await prisma.$executeRawUnsafe(
"UPDATE User SET secret = 'ENC:garbage' WHERE email = '[email protected]'"
);
const u = await prisma.user.findUnique({ where: { email: "[email protected]" } });
console.log("READ AFTER POISON:", u);
}
main()
.then(() => console.log("DONE poison.cjs"))
.catch(console.error)
.finally(() => prisma.$disconnect());
Output:
START poison.cjs
READ AFTER POISON: { id: 4, email: '[email protected]', secret: 'ENC:garbage' }
DONE poison.cjs
Even in strict mode, non-ciphertext values are returned without error.
Expected vs actual behavior
Expected (strict mode):
Only valid ciphertext accepted
Decryption failure → error
Non-ciphertext values → error
Actual:
Ciphertext → decrypted
Corrupted ciphertext → error
Non-ciphertext values → returned raw
Root cause analysis
Current logic appears to operate as:
If value matches ciphertext format → attempt decrypt
If decrypt fails and strict → throw
If value does not match ciphertext format → pass through as plaintext
Strict mode is only enforced when decryption is attempted. It is not enforced for values that do not resemble ciphertext.
This creates a fail-open behavior for non-ciphertext inputs in encrypted fields.
Suggested patch
Strict mode should reject any value that is not valid ciphertext.
Conceptual fix:
if (strictMode) {
if (!looksLikeCiphertext(value)) {
throw new Error("Strict mode: non-ciphertext value encountered");
}
}
Or:
if (strictMode) {
try {
return decrypt(value)
} catch {
throw new Error("Strict mode decryption failure")
}
}
Plaintext pass-through should not occur under strict mode.
Optional design improvements
Add explicit configuration:
fieldEncryptionExtension({
onDecryptError: "throw" | "null" | "redact"
})
or
strictRejectPlaintext: true
Severity assessment
This is not a cryptographic break.
It is a strict-mode enforcement inconsistency and integrity risk.
Relevant classification:
Integrity weakness
Fail-open behavior
Security hardening gap
Attack requires database write capability, but that is a realistic assumption in multiple threat models.
Conclusion
Strict mode currently protects against corrupted ciphertext but does not protect against non-ciphertext values stored in encrypted fields. Applications relying on strict mode for strong guarantees may unknowingly accept injected plaintext values.
The provided proof of concept demonstrates deterministic reproduction and should be sufficient for validation and remediation.