47ng / 47ng/prisma-field-encryption

Strict mode bypass in prisma-field-encryption allows non-ciphertext values to be returned in plaintext (fail-open behavior)

Offen
#152 2 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen
Vorherrschende Sprache
TypeScript
Sterne
306
Forks
40
PR-Merge-Kennzahlen
Keine gemergten PRs in 30 T.

Beschreibung

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: "a@a.com", 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: 'a@a.com', 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: "a@a.com", secret: "TOPSECRET" },
});

const rows = await prisma.$queryRawUnsafe(
"SELECT secret FROM User WHERE email = 'a@a.com'"
);

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 = 'a@a.com'",
corrupted
);

await prisma.user.findUnique({ where: { email: "a@a.com" } });
}

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: "a@a.com" },
update: { secret: "TOPSECRET" },
create: { email: "a@a.com", secret: "TOPSECRET" },
});

await prisma.$executeRawUnsafe(
"UPDATE User SET secret = 'ENC:garbage' WHERE email = 'a@a.com'"
);

const u = await prisma.user.findUnique({ where: { email: "a@a.com" } });
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: 'a@a.com', 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.

Beitragsleitfaden

Beitragsleitfaden öffnen

Rechercherichtung

The issue is in the strict mode logic of the prisma-field-encryption extension. Look at the decryption flow, likely in the main extension file or a dedicated decryption module. Identify where values are checked for ciphertext format and where strict mode is enforced. The fix is to ensure strict mode throws an error for any non-ciphertext value, not just for decryption failures. Test by reproducing the provided scripts to verify the bypass and then that the patch blocks it.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
sqlite, typescript
Bereich
databases, security
Issue-Typ
Bug
Schwierigkeit
3/5
Geschätzter Aufwand
1-2 Tage
Aktivitätsstatus
Veraltet
Klarheit
Klar beschrieben
Anfängerfreundlichkeit
45/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.