feat: implement prompt injection protection and OpenRouter paid API key support
- Add comprehensive prompt injection security module with 160+ attack pattern detection - Implement security checks in message handling with proper blocking and user feedback - Add OpenRouter paid API key support (OPENROUTER_PAID_API_KEY) for premium models - Update model discovery and chat functions to use paid API key for premium models - Add comprehensive test suite with 434 test cases (98.39% accuracy) - Tests cover legitimate WordPress development queries, injection attacks, obfuscated attempts - Improve builder loading indicators with text-based progress (building/planning) - Replace spinning animations with 'Starting build/planning process' messages
This commit is contained in:
@@ -14,6 +14,7 @@ const bcrypt = require('bcrypt');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const nodemailer = require('nodemailer');
|
||||
const PDFDocument = require('pdfkit');
|
||||
const security = require('./security');
|
||||
|
||||
let sharp = null;
|
||||
try {
|
||||
@@ -106,6 +107,8 @@ const REPO_ROOT = process.env.CHAT_REPO_ROOT || process.cwd();
|
||||
const DEFAULT_OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||
const OPENROUTER_API_URL = process.env.OPENROUTER_API_URL || DEFAULT_OPENROUTER_API_URL;
|
||||
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY || process.env.OPENROUTER_API_TOKEN || '';
|
||||
// Separate paid API key for premium models (optional)
|
||||
const OPENROUTER_PAID_API_KEY = process.env.OPENROUTER_PAID_API_KEY || process.env.OPENROUTER_PAID_API_TOKEN || OPENROUTER_API_KEY;
|
||||
const OPENCODE_OLLAMA_API_KEY = process.env.OPENCODE_OLLAMA_API_KEY || '';
|
||||
const OPENCODE_OLLAMA_BASE_URL = process.env.OPENCODE_OLLAMA_BASE_URL || 'https://ollama.plugincompass.com';
|
||||
const OPENCODE_OLLAMA_MODEL = process.env.OPENCODE_OLLAMA_MODEL || 'qwen3:0.6b';
|
||||
@@ -1900,29 +1903,30 @@ function checkHoneypot(body) {
|
||||
return !!(body.website && body.website.length > 0);
|
||||
}
|
||||
|
||||
// Security: Prompt Injection Protection
|
||||
function sanitizePromptInput(input) {
|
||||
// Security: Prompt Injection Protection - Comprehensive
|
||||
function sanitizePromptInput(input, options = {}) {
|
||||
if (!input || typeof input !== 'string') return '';
|
||||
|
||||
const patterns = [
|
||||
/ignore\s+previous\s+instructions/gi,
|
||||
/system\s*:/gi,
|
||||
/assistant\s*:/gi,
|
||||
/role\s*=\s*["']?system["']?/gi,
|
||||
/{{[^}]*}}/g,
|
||||
/```\s*ignore/gi,
|
||||
/\0/g,
|
||||
/eval\s*\(/gi,
|
||||
/exec\s*\(/gi,
|
||||
/process\./gi,
|
||||
];
|
||||
// Use the comprehensive security module
|
||||
const result = security.sanitizeUserInput(input, {
|
||||
strictMode: options.strictMode || false,
|
||||
maxLength: options.maxLength || MAX_PROMPT_LENGTH,
|
||||
allowMarkup: options.allowMarkup || false,
|
||||
logViolations: options.logViolations !== false // default true
|
||||
});
|
||||
|
||||
let result = input;
|
||||
for (const pattern of patterns) {
|
||||
result = result.replace(pattern, '[FILTERED]');
|
||||
// If blocked, return empty string (will be handled by caller)
|
||||
if (result.blocked) {
|
||||
return '[BLOCKED]';
|
||||
}
|
||||
|
||||
return result.slice(0, MAX_PROMPT_LENGTH).trim();
|
||||
return result.sanitized;
|
||||
}
|
||||
|
||||
// Security: Check if input should be blocked (for pre-validation)
|
||||
function shouldBlockUserInput(input) {
|
||||
if (!input || typeof input !== 'string') return { blocked: false };
|
||||
return security.shouldBlockInput(input);
|
||||
}
|
||||
|
||||
// Security: Output Validation
|
||||
@@ -7497,10 +7501,12 @@ function runCommand(command, args, options = {}) {
|
||||
|
||||
// Fetch models from OpenRouter API when API key is configured
|
||||
async function fetchOpenRouterModels() {
|
||||
if (!OPENROUTER_API_KEY) return [];
|
||||
// Try paid API key first, then fall back to regular key
|
||||
const apiKey = OPENROUTER_PAID_API_KEY || OPENROUTER_API_KEY;
|
||||
if (!apiKey) return [];
|
||||
try {
|
||||
const res = await fetch('https://openrouter.ai/api/v1/models', {
|
||||
headers: { 'Authorization': `Bearer ${OPENROUTER_API_KEY}` }
|
||||
headers: { 'Authorization': `Bearer ${apiKey}` }
|
||||
});
|
||||
if (!res.ok) {
|
||||
log('OpenRouter models fetch failed', { status: res.status });
|
||||
@@ -7980,7 +7986,21 @@ Create a concise, actionable plan: key features, WordPress hooks/APIs, data mode
|
||||
}
|
||||
|
||||
async function sendOpenRouterChat({ messages, model }) {
|
||||
if (!OPENROUTER_API_KEY) {
|
||||
// Determine which API key to use based on model
|
||||
// Premium/paid models use OPENROUTER_PAID_API_KEY
|
||||
const isPremiumModel = model && (
|
||||
model.includes('gpt-4') ||
|
||||
model.includes('claude-3-opus') ||
|
||||
model.includes('claude-3-5-sonnet') ||
|
||||
model.includes('gemini-1.5-pro') ||
|
||||
model.includes('llama-3.1-405b')
|
||||
);
|
||||
|
||||
const apiKey = isPremiumModel && OPENROUTER_PAID_API_KEY
|
||||
? OPENROUTER_PAID_API_KEY
|
||||
: OPENROUTER_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
log('OpenRouter API key missing, cannot fulfill planning request');
|
||||
throw new Error('OpenRouter API key is not configured');
|
||||
}
|
||||
@@ -7995,7 +8015,7 @@ async function sendOpenRouterChat({ messages, model }) {
|
||||
const payload = { model: model || resolveOpenRouterModel(), messages: safeMessages };
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${OPENROUTER_API_KEY}`,
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
};
|
||||
if (OPENROUTER_SITE_URL) headers['HTTP-Referer'] = OPENROUTER_SITE_URL;
|
||||
headers['X-Title'] = OPENROUTER_APP_NAME;
|
||||
@@ -14608,7 +14628,20 @@ async function handleNewMessage(req, res, sessionId, userId) {
|
||||
try {
|
||||
await ensureSessionPaths(session);
|
||||
const body = await parseJsonBody(req);
|
||||
const content = sanitizeMessage(body.content || '');
|
||||
|
||||
// Security: Check for prompt injection attacks
|
||||
const rawContent = body.content || '';
|
||||
const securityCheck = shouldBlockUserInput(rawContent);
|
||||
if (securityCheck.blocked) {
|
||||
return sendJson(res, 400, {
|
||||
error: 'Message blocked',
|
||||
blocked: true,
|
||||
reason: securityCheck.reason,
|
||||
message: 'This message was blocked due to potential security concerns. If you believe this is an error, please contact support.'
|
||||
});
|
||||
}
|
||||
|
||||
const content = sanitizeMessage(rawContent);
|
||||
const displayContent = typeof body.displayContent === 'string' && body.displayContent.trim()
|
||||
? body.displayContent.trim()
|
||||
: content;
|
||||
|
||||
Reference in New Issue
Block a user