mirror of
https://github.com/penpot/penpot.git
synced 2026-08-10 23:08:41 +00:00
Fix LDAP injection vulnerability (T5-N1-03) where the client-supplied email was used directly in the LDAP search filter without escaping RFC 4515 special characters (*, (, ), \, NUL), and the profile email was taken from client input instead of the LDAP directory attribute. Changes: - Add escape-ldap-filter-value per RFC 4515 section 3 - Apply escaping in search-user before building LDAP filter - Add get-attr helper for multi-valued LDAP attributes - Fix retrieve-user to use directory email (attrs-email) instead of client email - Use cuerdas.core instead of clojure.string Closes #11084 AI-assisted-by: mimo-v2.5-pro
107 lines
3.9 KiB
JavaScript
107 lines
3.9 KiB
JavaScript
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { rpcPost, extractCookie } from "./helpers/client.mjs";
|
|
|
|
async function loginWithLdap(email, password) {
|
|
const res = await rpcPost("login-with-ldap", { email, password });
|
|
if (res.status !== 200 || res.body.type) {
|
|
throw new Error(
|
|
`LDAP login failed: ${JSON.stringify(res.body)}`
|
|
);
|
|
}
|
|
const cookie = extractCookie(res.setCookie);
|
|
return { profile: res.body, cookie };
|
|
}
|
|
|
|
describe("LDAP injection — T5-N1-03", () => {
|
|
|
|
it("normal LDAP login works with valid credentials", async () => {
|
|
const { profile, cookie } = await loginWithLdap(
|
|
"fry@planetexpress.com",
|
|
"fry"
|
|
);
|
|
assert.equal(profile.email, "fry@planetexpress.com");
|
|
assert.ok(profile.id, "profile should have id");
|
|
assert.ok(cookie, "cookie should be set");
|
|
});
|
|
|
|
it("wildcard injection: *@planetexpress.com must not return client literal as email", async () => {
|
|
// ATTACK SCENARIO (from Criptored audit):
|
|
// 1. Attacker (amy) sends email="*@planetexpress.com" with her own password
|
|
// 2. LDAP filter becomes (mail=*@planetexpress.com) — * is a wildcard
|
|
// 3. With sizelimit=1, LDAP returns amy's entry (first match)
|
|
// 4. Bind succeeds: amy's DN + amy's password = valid
|
|
//
|
|
// EXPECTED BEHAVIOR AFTER FIX (two valid outcomes):
|
|
// A) If * is escaped: LDAP finds no match → wrong-credentials (injection blocked)
|
|
// B) If * matches: profile email must be "amy@planetexpress.com" (directory), not "*@planetexpress.com" (client)
|
|
//
|
|
// Either outcome is correct — the vulnerability is fixed.
|
|
try {
|
|
const { profile } = await loginWithLdap("*@planetexpress.com", "amy");
|
|
// Outcome B: login succeeded, verify email is from directory
|
|
assert.equal(
|
|
profile.email,
|
|
"amy@planetexpress.com",
|
|
"email must come from LDAP directory, not client input"
|
|
);
|
|
} catch (e) {
|
|
// Outcome A: injection blocked — * is escaped, no LDAP match
|
|
assert.ok(
|
|
e.message.includes("wrong-credentials"),
|
|
"wildcard should be rejected or return directory email"
|
|
);
|
|
}
|
|
});
|
|
|
|
it("identity swap: alternate email must return primary directory email", async () => {
|
|
// Professor has two emails in LDAP: professor@ and hubert@.
|
|
// Login with hubert@ — the profile email should be the one
|
|
// the LDAP directory returns as attrs-email, not what the client typed.
|
|
//
|
|
// EXPECTED BEHAVIOR AFTER FIX:
|
|
// Profile email should be "professor@planetexpress.com" (primary directory email),
|
|
// NOT "hubert@planetexpress.com" (client literal).
|
|
//
|
|
// CURRENT BUG: email is "hubert@planetexpress.com" (client literal) — test FAILS
|
|
const { profile, cookie } = await loginWithLdap(
|
|
"hubert@planetexpress.com",
|
|
"professor"
|
|
);
|
|
assert.ok(profile.id, "profile should have id");
|
|
assert.ok(cookie, "cookie should be set");
|
|
// This assertion FAILS with current code (RED) — proves the vulnerability
|
|
assert.equal(
|
|
profile.email,
|
|
"professor@planetexpress.com",
|
|
"email must come from LDAP directory, not client input"
|
|
);
|
|
});
|
|
|
|
it("wrong password fails", async () => {
|
|
try {
|
|
await loginWithLdap("fry@planetexpress.com", "wrong-password");
|
|
assert.fail("should have thrown");
|
|
} catch (e) {
|
|
assert.ok(
|
|
e.message.includes("LDAP login failed") ||
|
|
e.message.includes("wrong-credentials"),
|
|
"should fail with wrong credentials"
|
|
);
|
|
}
|
|
});
|
|
|
|
it("non-existent user fails", async () => {
|
|
try {
|
|
await loginWithLdap("nobody@planetexpress.com", "password");
|
|
assert.fail("should have thrown");
|
|
} catch (e) {
|
|
assert.ok(
|
|
e.message.includes("LDAP login failed") ||
|
|
e.message.includes("wrong-credentials"),
|
|
"should fail for non-existent user"
|
|
);
|
|
}
|
|
});
|
|
});
|