-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Ensure setup parameters are sanitized
Signed-off-by: Lucian Buzzo <lucian.buzzo@gmail.com>
- Loading branch information
1 parent
5e167ab
commit 0d302ab
Showing
2 changed files
with
303 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
import { PrismaClient } from "@prisma/client"; | ||
import { v4 as uuid } from "uuid"; | ||
import { setup, SetupParams } from "../../src"; | ||
|
||
let adminClient: PrismaClient; | ||
|
||
beforeAll(async () => { | ||
adminClient = new PrismaClient(); | ||
}); | ||
|
||
// https://xkcd.com/327/ | ||
const BAD_STRING = "Robert'); DROP TABLE STUDENTS; --"; | ||
|
||
describe("sanitation", () => { | ||
it("should sanitize role names", async () => { | ||
const prisma = new PrismaClient(); | ||
const role = BAD_STRING; | ||
|
||
await setup({ | ||
prisma, | ||
getRoles(abilities) { | ||
return { | ||
[role]: [abilities.Post.read], | ||
}; | ||
}, | ||
getContext: () => ({ | ||
role, | ||
}), | ||
}); | ||
|
||
const { id: postId } = await adminClient.post.create({ | ||
data: { | ||
title: `Test post from ${role}`, | ||
}, | ||
}); | ||
|
||
const post = await prisma.post.findUnique({ | ||
where: { id: postId }, | ||
}); | ||
|
||
expect(post?.id).toBe(postId); | ||
}); | ||
|
||
it("should sanitize ability names", async () => { | ||
const prisma = new PrismaClient(); | ||
const role = `USER_${uuid()}`; | ||
const ability = BAD_STRING; | ||
await setup({ | ||
prisma, | ||
customAbilities: { | ||
Post: { | ||
[ability]: { | ||
description: "Test Post Read", | ||
operation: "SELECT", | ||
expression: "true", | ||
}, | ||
}, | ||
}, | ||
getRoles(abilities) { | ||
return { | ||
[role]: [abilities.Post[ability]], | ||
}; | ||
}, | ||
getContext: () => ({ | ||
role, | ||
}), | ||
}); | ||
|
||
const { id: postId } = await adminClient.post.create({ | ||
data: { | ||
title: `Test post from ${role}`, | ||
}, | ||
}); | ||
|
||
const post = await prisma.post.findUnique({ | ||
where: { id: postId }, | ||
}); | ||
|
||
expect(post?.id).toBe(postId); | ||
}); | ||
|
||
it("should sanitize operations", async () => { | ||
const prisma = new PrismaClient(); | ||
const role = `USER_${uuid()}`; | ||
const ability = "customAbility"; | ||
|
||
await expect( | ||
setup({ | ||
prisma, | ||
customAbilities: { | ||
Post: { | ||
[ability]: { | ||
description: "Test Post Read", | ||
operation: BAD_STRING as any, | ||
expression: "true", | ||
}, | ||
}, | ||
}, | ||
getRoles(abilities) { | ||
return { | ||
[role]: [abilities.Post[ability]], | ||
}; | ||
}, | ||
getContext: () => ({ | ||
role, | ||
}), | ||
}), | ||
).rejects.toThrowError("Invalid operation"); | ||
}); | ||
|
||
it("should sanitize model names", async () => { | ||
const prisma = new PrismaClient(); | ||
const role = `USER_${uuid()}`; | ||
const ability = "customAbility"; | ||
|
||
await expect( | ||
setup({ | ||
prisma, | ||
customAbilities: { | ||
[BAD_STRING]: { | ||
[ability]: { | ||
description: "Test Post Read", | ||
operation: "SELECT", | ||
expression: "true", | ||
}, | ||
}, | ||
} as any, | ||
getRoles(abilities) { | ||
return { | ||
[role]: [(abilities as any)[BAD_STRING][ability]], | ||
}; | ||
}, | ||
getContext: () => ({ | ||
role, | ||
}), | ||
}), | ||
).rejects.toThrowError("Invalid models in custom abilities"); | ||
}); | ||
|
||
it("should sanitize custom context keys", async () => { | ||
const prisma = new PrismaClient(); | ||
|
||
const role = `USER_${uuid()}`; | ||
|
||
const postTitle = `Test post from ${role}`; | ||
|
||
const config: SetupParams = { | ||
prisma, | ||
customAbilities: { | ||
Post: { | ||
createWithTitle: { | ||
description: "Test Post Create", | ||
operation: "INSERT", | ||
expression: "current_setting('post.title') = title", | ||
}, | ||
}, | ||
}, | ||
getRoles(abilities) { | ||
return { | ||
[role]: [abilities.Post.createWithTitle, abilities.Post.read], | ||
}; | ||
}, | ||
getContext: () => { | ||
return { | ||
role, | ||
context: { | ||
[BAD_STRING]: postTitle, | ||
}, | ||
}; | ||
}, | ||
}; | ||
|
||
await setup(config); | ||
|
||
await expect( | ||
prisma.post.create({ | ||
data: { | ||
title: BAD_STRING, | ||
}, | ||
}), | ||
).rejects.toThrow(`Context variable "${BAD_STRING}" contains invalid characters`); | ||
}); | ||
|
||
it("should sanitize custom context values", async () => { | ||
const prisma = new PrismaClient(); | ||
|
||
const role = `USER_${uuid()}`; | ||
|
||
const postTitle = `Test post from ${role}`; | ||
|
||
const config: SetupParams = { | ||
prisma, | ||
customAbilities: { | ||
Post: { | ||
createWithTitle: { | ||
description: "Test Post Create", | ||
operation: "INSERT", | ||
expression: "current_setting('post.title') = title", | ||
}, | ||
}, | ||
}, | ||
getRoles(abilities) { | ||
return { | ||
[role]: [abilities.Post.createWithTitle, abilities.Post.read], | ||
}; | ||
}, | ||
getContext: () => { | ||
return { | ||
role, | ||
context: { | ||
"post.title": BAD_STRING, | ||
}, | ||
}; | ||
}, | ||
}; | ||
|
||
await setup(config); | ||
|
||
// We use a prepared statement to sanitize the value, so the expectation | ||
// is that it would simply not match and fail the RLS check, rather than throwing an error | ||
await expect( | ||
prisma.post.create({ | ||
data: { | ||
title: postTitle, | ||
}, | ||
}), | ||
).rejects.toThrow("You do not have permission to perform this action"); | ||
}); | ||
|
||
// Note: SQL check expressions are inherently unsafe, so we don't sanitize them | ||
// Postgres will throw an error if the expression is invalid, which gives us some safety, however | ||
// the ideal solution is to use a query builder for the expression | ||
it("should sanitize custom ability expressions", async () => { | ||
const prisma = new PrismaClient(); | ||
|
||
const role = `USER_${uuid()}`; | ||
|
||
const config: SetupParams = { | ||
prisma, | ||
customAbilities: { | ||
Post: { | ||
createWithTitle: { | ||
description: "Test Post Create", | ||
operation: "INSERT", | ||
expression: 'DROP TABLE "Post"', | ||
}, | ||
}, | ||
}, | ||
getRoles(abilities) { | ||
return { | ||
[role]: [abilities.Post.createWithTitle, abilities.Post.read], | ||
}; | ||
}, | ||
getContext: () => { | ||
return { | ||
role, | ||
context: { | ||
"post.title": BAD_STRING, | ||
}, | ||
}; | ||
}, | ||
}; | ||
|
||
await expect(setup(config)).rejects.toThrow("syntax error"); | ||
}); | ||
}); |