security: cap assistant chat payload + injection-guard project cover prompt (#38)
`messages[].content` and `pageContext` had no `.max()` — a single chat turn could ship 50 MB / 200 messages and OOM JSON.parse, balloon prompt assembly, and burn arbitrary AI-provider cost. Separately, the project-cover image-generation path concatenated user free-text into the DALL-E / Gemini prompt without any injection check, so a manager could pivot the image model into "ignore previous instructions" / role-override style attacks against downstream prompt-aware infra. - assistant-procedure-support: add `.max(10_000)` per message, `.max(2_000)` on pageContext, and a `.superRefine` aggregate cap (200 KB total bytes across all messages + page context). Constants exported so call sites and tests share one source of truth. - project-cover.generateCover: run `checkPromptInjection` over the user-supplied `prompt` field; reject with BAD_REQUEST on match. - 7 schema-bound tests covering per-message, page-context, aggregate, message-count, and happy-path cases. Covers EAPPS 3.2.7 (input bounds) / EGAI 4.6.3.2 (prompt-injection detection on user inputs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { createDalleClient, isDalleConfigured, loggedAiCall, parseAiError } from
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
import { generateGeminiImage, isGeminiConfigured, parseGeminiError } from "../gemini-client.js";
|
||||
import { validateImageDataUrl } from "../lib/image-validation.js";
|
||||
import { checkPromptInjection } from "../lib/prompt-guard.js";
|
||||
import { resolveSystemSettingsRuntime } from "../lib/system-settings-runtime.js";
|
||||
import { managerProcedure, protectedProcedure, requirePermission } from "../trpc.js";
|
||||
|
||||
@@ -19,9 +20,8 @@ async function readImageGenerationStatus(db: {
|
||||
where: { id: "singleton" },
|
||||
});
|
||||
const imageProvider = settings?.["imageProvider"] === "gemini" ? "gemini" : "dalle";
|
||||
const configured = imageProvider === "gemini"
|
||||
? isGeminiConfigured(settings)
|
||||
: isDalleConfigured(settings);
|
||||
const configured =
|
||||
imageProvider === "gemini" ? isGeminiConfigured(settings) : isDalleConfigured(settings);
|
||||
|
||||
return {
|
||||
configured,
|
||||
@@ -31,13 +31,30 @@ async function readImageGenerationStatus(db: {
|
||||
|
||||
export const projectCoverProcedures = {
|
||||
generateCover: managerProcedure
|
||||
.input(z.object({
|
||||
projectId: z.string(),
|
||||
prompt: z.string().max(500).optional(),
|
||||
}))
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
prompt: z.string().max(500).optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
|
||||
|
||||
// The user's free-text "Additional direction" is concatenated into the
|
||||
// image-generation prompt. Run the same injection guard we apply to
|
||||
// assistant chat (EGAI 4.6.3.2) so a manager-role user can't pivot the
|
||||
// image model into "ignore previous instructions" / role-override
|
||||
// attacks against downstream prompt-aware infra.
|
||||
if (input.prompt) {
|
||||
const guard = checkPromptInjection(input.prompt);
|
||||
if (!guard.safe) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Prompt rejected: contains an injection pattern.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const project = await findUniqueOrThrow(
|
||||
ctx.db.project.findUnique({
|
||||
where: { id: input.projectId },
|
||||
@@ -85,7 +102,10 @@ export const projectCoverProcedures = {
|
||||
}
|
||||
} else {
|
||||
const dalleClient = createDalleClient(runtimeSettings);
|
||||
const model = runtimeSettings.aiProvider === "azure" ? runtimeSettings.azureDalleDeployment! : "dall-e-3";
|
||||
const model =
|
||||
runtimeSettings.aiProvider === "azure"
|
||||
? runtimeSettings.azureDalleDeployment!
|
||||
: "dall-e-3";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let response: any;
|
||||
@@ -126,10 +146,12 @@ export const projectCoverProcedures = {
|
||||
}),
|
||||
|
||||
uploadCover: managerProcedure
|
||||
.input(z.object({
|
||||
projectId: z.string(),
|
||||
imageDataUrl: z.string(),
|
||||
}))
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
imageDataUrl: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
|
||||
|
||||
@@ -187,10 +209,12 @@ export const projectCoverProcedures = {
|
||||
}),
|
||||
|
||||
updateCoverFocus: managerProcedure
|
||||
.input(z.object({
|
||||
projectId: z.string(),
|
||||
coverFocusY: z.number().int().min(0).max(100),
|
||||
}))
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
coverFocusY: z.number().int().min(0).max(100),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
|
||||
await ctx.db.project.update({
|
||||
@@ -200,13 +224,13 @@ export const projectCoverProcedures = {
|
||||
return { ok: true };
|
||||
}),
|
||||
|
||||
isImageGenConfigured: protectedProcedure
|
||||
.query(async ({ ctx }) => readImageGenerationStatus(ctx.db)),
|
||||
isImageGenConfigured: protectedProcedure.query(async ({ ctx }) =>
|
||||
readImageGenerationStatus(ctx.db),
|
||||
),
|
||||
|
||||
/** @deprecated Use isImageGenConfigured instead */
|
||||
isDalleConfigured: protectedProcedure
|
||||
.query(async ({ ctx }) => {
|
||||
const { configured } = await readImageGenerationStatus(ctx.db);
|
||||
return { configured };
|
||||
}),
|
||||
isDalleConfigured: protectedProcedure.query(async ({ ctx }) => {
|
||||
const { configured } = await readImageGenerationStatus(ctx.db);
|
||||
return { configured };
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user