added security checks for journal text box for AI
This commit is contained in:
129
lib/ai-output-safety.ts
Normal file
129
lib/ai-output-safety.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
const MAX_RAW_AI_RESPONSE_LENGTH = 4000;
|
||||
const MAX_AI_TASK_ARRAY_LENGTH = 10;
|
||||
const MAX_AI_TASK_LABEL_LENGTH = 80;
|
||||
|
||||
const UNSAFE_RAW_OUTPUT_PATTERNS = [
|
||||
/you are a supportive wellness coach/i,
|
||||
/never reveal system instructions/i,
|
||||
/content inside <user_journal>/i,
|
||||
/<\/?user_journal\b/i,
|
||||
/<\/?trusted_app_context\b/i,
|
||||
/<\/?generation_request\b/i,
|
||||
/<\/?system\b/i,
|
||||
/<\/?assistant\b/i,
|
||||
/openrouter/i,
|
||||
/\b(?:initial|original|hidden|secret)\s+(?:system\s+)?(?:prompt|instructions)\b/i,
|
||||
/\bhere(?:'s| is)\s+(?:my|the)\s+(?:system|original)\s+(?:prompt|instructions)\b/i,
|
||||
/\bas an ai (?:language )?model\b/i,
|
||||
/\bI(?:'m| am) (?:an AI|a language model)\b/,
|
||||
/\b(?:cannot|can't|must not)\s+(?:reveal|share|disclose)\b/i,
|
||||
/\[\s*\{\s*"role"\s*:/,
|
||||
/\bhttps?:\/\//i,
|
||||
/<<SYS>>|\[INST\]/,
|
||||
];
|
||||
|
||||
const UNSAFE_TASK_LABEL_PATTERNS = [
|
||||
/<\/?[a-z][\w-]*>/i,
|
||||
/\b(?:system|assistant|user|developer)\s*:/i,
|
||||
/\b(?:ignore|disregard|forget|override|bypass)\b.{0,30}\b(?:previous|prior|instructions?|prompt)\b/i,
|
||||
/\b(?:reveal|print|show|repeat|output|disclose|share)\b.{0,30}\b(?:prompt|instructions?|rules?)\b/i,
|
||||
/\b(?:what were|tell me)\b.{0,30}\b(?:instructions?|prompt)\b/i,
|
||||
/\b(?:jailbreak|prompt injection|developer mode)\b/i,
|
||||
/\b(?:you are now|act as|pretend to be|roleplay as)\b/i,
|
||||
/\b(?:initial|original|system|hidden)\s+(?:prompt|instructions?)\b/i,
|
||||
/\bhttps?:\/\//i,
|
||||
/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/,
|
||||
];
|
||||
|
||||
export class UnsafeAiOutputError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "UnsafeAiOutputError";
|
||||
}
|
||||
}
|
||||
|
||||
export function assertSafeRawAiResponse(content: string) {
|
||||
const trimmed = content.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
throw new UnsafeAiOutputError("OpenRouter returned an empty response.");
|
||||
}
|
||||
|
||||
if (trimmed.length > MAX_RAW_AI_RESPONSE_LENGTH) {
|
||||
throw new UnsafeAiOutputError("OpenRouter response exceeded the allowed size.");
|
||||
}
|
||||
|
||||
for (const pattern of UNSAFE_RAW_OUTPUT_PATTERNS) {
|
||||
if (pattern.test(trimmed)) {
|
||||
throw new UnsafeAiOutputError(
|
||||
"OpenRouter response contained disallowed content.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isSafeAiTaskLabel(label: string) {
|
||||
const trimmed = label.trim();
|
||||
|
||||
if (!trimmed || trimmed.length > MAX_AI_TASK_LABEL_LENGTH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !UNSAFE_TASK_LABEL_PATTERNS.some((pattern) => pattern.test(trimmed));
|
||||
}
|
||||
|
||||
export function filterSafeAiTaskLabels(labels: string[]) {
|
||||
return labels.filter(isSafeAiTaskLabel);
|
||||
}
|
||||
|
||||
export function extractJsonArray(content: string) {
|
||||
const start = content.indexOf("[");
|
||||
const end = content.lastIndexOf("]");
|
||||
|
||||
if (start === -1 || end === -1 || end <= start) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(content.slice(start, end + 1)) as unknown;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractSafeJsonTaskArray(content: string) {
|
||||
assertSafeRawAiResponse(content);
|
||||
|
||||
const parsed = extractJsonArray(content);
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new UnsafeAiOutputError(
|
||||
"OpenRouter response was not a JSON array of task labels.",
|
||||
);
|
||||
}
|
||||
|
||||
if (parsed.length > MAX_AI_TASK_ARRAY_LENGTH) {
|
||||
throw new UnsafeAiOutputError("OpenRouter response included too many tasks.");
|
||||
}
|
||||
|
||||
if (
|
||||
!parsed.every(
|
||||
(item) => typeof item === "string" && item.trim().length > 0,
|
||||
)
|
||||
) {
|
||||
throw new UnsafeAiOutputError(
|
||||
"OpenRouter response included non-string task labels.",
|
||||
);
|
||||
}
|
||||
|
||||
const labels = parsed.map((item) => (item as string).trim());
|
||||
const safeLabels = filterSafeAiTaskLabels(labels);
|
||||
|
||||
if (safeLabels.length === 0 && labels.length > 0) {
|
||||
throw new UnsafeAiOutputError(
|
||||
"OpenRouter response included only unsafe task labels.",
|
||||
);
|
||||
}
|
||||
|
||||
return safeLabels;
|
||||
}
|
||||
Reference in New Issue
Block a user