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:
southseact-3d
2026-02-08 13:23:59 +00:00
parent 6e0d039d7c
commit 0f631dc99a
6 changed files with 1440 additions and 39 deletions

View File

@@ -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;