From ea03db3d451fed6d69805cf97ce1d69d77a57242 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 14 Apr 2026 05:31:54 +0200 Subject: [PATCH] Prompt-injection wrap: remaining AI routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied the delimiter + INJECTION_GUARD pattern to: - src/routes/sickVisit.js (chief complaint, transcript, dictation, ROS, physical exam, diagnoses, style hints) - src/routes/wellVisit.js (SSHADESS answers + full well-visit context) - src/routes/chartReview.js (PMH + all visit content + labs) - src/routes/hospitalCourse.js (all notes/H&P/ED + clarification & update endpoints) - src/routes/milestones.js (narrative + summary) Each wraps patient-derived text in tags and appends the INJECTION_GUARD system instruction that tells the model to treat wrapped content strictly as data. Operator- supplied `additionalInstructions` stays unwrapped (trusted). --- src/routes/chartReview.js | 21 +++++++------- src/routes/hospitalCourse.js | 39 ++++++++++++-------------- src/routes/milestones.js | 9 +++--- src/routes/sickVisit.js | 28 +++++++------------ src/routes/wellVisit.js | 53 ++++++++++++------------------------ 5 files changed, 60 insertions(+), 90 deletions(-) diff --git a/src/routes/chartReview.js b/src/routes/chartReview.js index 2cb6378..63e69fd 100644 --- a/src/routes/chartReview.js +++ b/src/routes/chartReview.js @@ -4,6 +4,7 @@ const { callAI } = require('../utils/ai'); const PROMPTS = require('../utils/prompts'); const { authMiddleware } = require('../middleware/auth'); var logger = require('../utils/logger'); +var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe'); // Generate chart review router.post('/generate-chart-review', authMiddleware, async (req, res) => { @@ -21,7 +22,7 @@ router.post('/generate-chart-review', authMiddleware, async (req, res) => { const today = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); let clinicalData = `Today's date: ${today}\nPatient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}`; - if (pmh) clinicalData += `\nPMH: ${pmh}`; + if (pmh) clinicalData += `\nPMH: ${wrapUserText('pmh', pmh)}`; // Use the top-level review type to select the prompt — // the per-visit types only control data formatting, not the prompt. @@ -37,29 +38,29 @@ router.post('/generate-chart-review', authMiddleware, async (req, res) => { // Include ALL visit data regardless of per-visit type — // an outpatient chart review should include subspecialty consults too (visits || []).forEach(v => { - clinicalData += `\n\n=== OUTPATIENT VISIT (${v.date}) ===\n${v.content}`; - if (v.labs && v.labs.trim()) clinicalData += `\n--- Labs from this visit (${v.date}) ---\n${v.labs}`; + clinicalData += `\n\n=== OUTPATIENT VISIT (${v.date}) ===\n` + wrapUserText('visit', v.content || ''); + if (v.labs && v.labs.trim()) clinicalData += `\n--- Labs from this visit (${v.date}) ---\n` + wrapUserText('labs', v.labs); }); (subspecialty || []).forEach(s => { - clinicalData += `\n\n=== SUBSPECIALTY: ${(s.specialty || 'Subspecialty').toUpperCase()} — ${s.specialistName || 'Unknown'} (${s.date}) ===\n${s.content}`; - if (s.labs && s.labs.trim()) clinicalData += `\n--- Labs from this visit (${s.date}) ---\n${s.labs}`; + clinicalData += `\n\n=== SUBSPECIALTY: ${(s.specialty || 'Subspecialty').toUpperCase()} — ${s.specialistName || 'Unknown'} (${s.date}) ===\n` + wrapUserText('consult', s.content || ''); + if (s.labs && s.labs.trim()) clinicalData += `\n--- Labs from this visit (${s.date}) ---\n` + wrapUserText('labs', s.labs); }); (edVisits || []).forEach(v => { - clinicalData += `\n\n=== ED VISIT (${v.date}) ===\n${v.content}`; - if (v.labs && v.labs.trim()) clinicalData += `\n--- Labs from this visit (${v.date}) ---\n${v.labs}`; + clinicalData += `\n\n=== ED VISIT (${v.date}) ===\n` + wrapUserText('ed_visit', v.content || ''); + if (v.labs && v.labs.trim()) clinicalData += `\n--- Labs from this visit (${v.date}) ---\n` + wrapUserText('labs', v.labs); }); if (labs && labs.length > 0) { clinicalData += '\n\n=== ADDITIONAL LABS (not tied to a specific visit) ==='; - labs.forEach(l => { clinicalData += `\n${l.date ? l.date + ': ' : ''}${l.values}`; }); + labs.forEach(l => { clinicalData += `\n${l.date ? l.date + ': ' : ''}` + wrapUserText('labs', l.values || ''); }); } if (additionalInstructions) { - prompt += `\n\nADDITIONAL INSTRUCTIONS:\n${additionalInstructions}`; + prompt += `\n\nADDITIONAL INSTRUCTIONS (operator-supplied, trusted):\n${additionalInstructions}`; } const result = await callAI([ - { role: 'system', content: prompt }, + { role: 'system', content: prompt + INJECTION_GUARD }, { role: 'user', content: clinicalData } ], { model, maxTokens: 4000 }); diff --git a/src/routes/hospitalCourse.js b/src/routes/hospitalCourse.js index e4de7a2..fe6c53e 100644 --- a/src/routes/hospitalCourse.js +++ b/src/routes/hospitalCourse.js @@ -4,6 +4,7 @@ const { callAI } = require('../utils/ai'); const PROMPTS = require('../utils/prompts'); const { authMiddleware } = require('../middleware/auth'); var logger = require('../utils/logger'); +var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe'); // Generate hospital course router.post('/generate-hospital-course', authMiddleware, async (req, res) => { @@ -51,41 +52,35 @@ router.post('/generate-hospital-course', authMiddleware, async (req, res) => { // Build the complete clinical data let clinicalData = `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}`; - if (pmh) clinicalData += `\nPMH: ${pmh}`; + if (pmh) clinicalData += `\nPMH: ` + wrapUserText('pmh', pmh); if (setting) clinicalData += `\nSetting: ${setting}`; if (los) clinicalData += `\nLength of Stay: ${los} days`; clinicalData += `\nFormat: ${format}`; if (edNote) { - clinicalData += `\n\n=== ED NOTE (${edNote.date || 'date unknown'}) ===\n${edNote.content}`; - if (edNote.labs) clinicalData += `\nED Labs: ${edNote.labs}`; + clinicalData += `\n\n=== ED NOTE (${edNote.date || 'date unknown'}) ===\n` + wrapUserText('ed_note', edNote.content || ''); + if (edNote.labs) clinicalData += `\nED Labs: ` + wrapUserText('labs', edNote.labs); } + if (hAndP) clinicalData += `\n\n=== H&P (${hAndP.date || 'date unknown'}) ===\n` + wrapUserText('h_and_p', hAndP.content || ''); - if (hAndP) { - clinicalData += `\n\n=== H&P (${hAndP.date || 'date unknown'}) ===\n${hAndP.content}`; - } - - // Sort notes by date const sortedNotes = [...notes].sort((a, b) => new Date(a.date) - new Date(b.date)); sortedNotes.forEach((note, i) => { - clinicalData += `\n\n=== ${(note.type || 'Progress Note').toUpperCase()} - ${note.date || `Note ${i + 1}`} ===\n${note.content}`; + clinicalData += `\n\n=== ${(note.type || 'Progress Note').toUpperCase()} - ${note.date || `Note ${i + 1}`} ===\n` + wrapUserText('note', note.content || ''); }); if (labs && labs.length > 0) { clinicalData += '\n\n=== LABS ==='; - labs.forEach(lab => { - clinicalData += `\n${lab.date}: ${lab.values}`; - }); + labs.forEach(lab => { clinicalData += `\n${lab.date}: ` + wrapUserText('labs', lab.values || ''); }); } if (additionalInstructions) { - prompt += `\n\nADDITIONAL INSTRUCTIONS FROM PHYSICIAN:\n${additionalInstructions}`; + prompt += `\n\nADDITIONAL INSTRUCTIONS FROM PHYSICIAN (operator-supplied, trusted):\n${additionalInstructions}`; } - - if (physicianMemories) clinicalData += '\n\n[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n' + physicianMemories + '\n[END STYLE HINTS]'; + if (physicianMemories) clinicalData += '\n\n[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n' + + wrapUserText('style_hints', physicianMemories) + '\n[END STYLE HINTS]'; const result = await callAI([ - { role: 'system', content: prompt }, + { role: 'system', content: prompt + INJECTION_GUARD }, { role: 'user', content: clinicalData } ], { model, maxTokens: 6000 }); @@ -107,8 +102,8 @@ router.post('/hospital-course-clarify', authMiddleware, async (req, res) => { const { currentDraft, notes } = req.body; const result = await callAI([ - { role: 'system', content: PROMPTS.askClarification }, - { role: 'user', content: `Current draft:\n${currentDraft}\n\nSource notes:\n${JSON.stringify(notes)}` } + { role: 'system', content: PROMPTS.askClarification + INJECTION_GUARD }, + { role: 'user', content: `Current draft:\n` + wrapUserText('draft', currentDraft || '') + `\n\nSource notes:\n` + wrapUserText('notes', JSON.stringify(notes || [])) } ], { model: req.body.model }); res.json({ success: true, questions: result.content, model: result.model }); @@ -123,15 +118,15 @@ router.post('/hospital-course-update', authMiddleware, async (req, res) => { const { currentCourse, updates, model, instructions } = req.body; if (!currentCourse) return res.status(400).json({ error: 'No current course' }); - let userMsg = `CURRENT HOSPITAL COURSE:\n${currentCourse}\n\n`; - if (updates) userMsg += `NEW UPDATES TO ADD:\n${updates}\n\n`; - if (instructions) userMsg += `INSTRUCTIONS:\n${instructions}`; + let userMsg = `CURRENT HOSPITAL COURSE:\n` + wrapUserText('course', currentCourse) + `\n\n`; + if (updates) userMsg += `NEW UPDATES TO ADD:\n` + wrapUserText('updates', updates) + `\n\n`; + if (instructions) userMsg += `INSTRUCTIONS:\n` + wrapUserText('instructions', instructions); const result = await callAI([ { role: 'system', content: `You are updating a hospital course document with new information. Add the new information in the appropriate place. Maintain the same format and style. If adding discharge day information, add it at the end. -${PROMPTS.refine.split('\n').slice(0, -1).join('\n')}` }, +${PROMPTS.refine.split('\n').slice(0, -1).join('\n')}` + INJECTION_GUARD }, { role: 'user', content: userMsg } ], { model, maxTokens: 6000 }); diff --git a/src/routes/milestones.js b/src/routes/milestones.js index 69b5e5f..58b4c62 100644 --- a/src/routes/milestones.js +++ b/src/routes/milestones.js @@ -5,6 +5,7 @@ const PROMPTS = require('../utils/prompts'); const { authMiddleware } = require('../middleware/auth'); const db = require('../db/database'); var logger = require('../utils/logger'); +var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe'); // Get all milestones for client (authenticated users) router.get('/milestones-data', authMiddleware, async (req, res) => { @@ -59,8 +60,8 @@ router.post('/generate-milestone-narrative', authMiddleware, async (req, res) => const prompt = (format === 'list') ? PROMPTS.milestoneList : PROMPTS.milestoneNarrative; const result = await callAI([ - { role: 'system', content: prompt }, - { role: 'user', content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\n${milestoneText}\n\nAssessed: ${achieved.length + notAchieved.length} | Achieved: ${achieved.length} | Not achieved: ${notAchieved.length} | Not assessed (OMIT): ${notAssessed.length}` } + { role: 'system', content: prompt + INJECTION_GUARD }, + { role: 'user', content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\n` + wrapUserText('milestones', milestoneText) + `\n\nAssessed: ${achieved.length + notAchieved.length} | Achieved: ${achieved.length} | Not achieved: ${notAchieved.length} | Not assessed (OMIT): ${notAssessed.length}` } ], { model }); res.json({ @@ -82,8 +83,8 @@ router.post('/generate-milestone-summary', authMiddleware, async (req, res) => { if (!narrative) return res.status(400).json({ error: 'No narrative' }); const result = await callAI([ - { role: 'system', content: PROMPTS.milestoneSummary }, - { role: 'user', content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\nNARRATIVE:\n${narrative}` } + { role: 'system', content: PROMPTS.milestoneSummary + INJECTION_GUARD }, + { role: 'user', content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\nNARRATIVE:\n` + wrapUserText('narrative', narrative) } ], { model, maxTokens: 500 }); res.json({ success: true, summary: result.content, model: result.model }); diff --git a/src/routes/sickVisit.js b/src/routes/sickVisit.js index 46815f7..3cd8a63 100644 --- a/src/routes/sickVisit.js +++ b/src/routes/sickVisit.js @@ -8,6 +8,7 @@ var { callAI } = require('../utils/ai'); var PROMPTS = require('../utils/prompts'); var { authMiddleware } = require('../middleware/auth'); var logger = require('../utils/logger'); +var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe'); // ── POST generate sick visit note ──────────────────────────────────────── router.post('/sick-visit/note', authMiddleware, async function(req, res) { @@ -28,32 +29,23 @@ router.post('/sick-visit/note', authMiddleware, async function(req, res) { // Assemble context var context = 'SICK VISIT\n'; context += 'Patient: ' + (patientAge || 'Unknown age') + ', ' + (patientGender || 'Unknown gender') + '\n'; - context += 'Chief Complaint: ' + chiefComplaint + '\n\n'; + context += 'Chief Complaint: ' + wrapUserText('chief_complaint', chiefComplaint) + '\n\n'; if (transcript) { - context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + transcript + '\n\n'; + context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + wrapUserText('transcript', transcript) + '\n\n'; } else if (dictation) { - context += 'PHYSICIAN DICTATION:\n' + dictation + '\n\n'; + context += 'PHYSICIAN DICTATION:\n' + wrapUserText('dictation', dictation) + '\n\n'; } - - if (ros) { - context += ros + '\n\n'; - } - - if (physicalExam) { - context += physicalExam + '\n\n'; - } - - if (diagnoses) { - context += diagnoses + '\n\n'; - } - + if (ros) context += wrapUserText('ros', ros) + '\n\n'; + if (physicalExam) context += wrapUserText('physical_exam', physicalExam) + '\n\n'; + if (diagnoses) context += wrapUserText('diagnoses', diagnoses) + '\n\n'; if (physicianMemories) { - context += '[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n' + physicianMemories + '\n[END STYLE HINTS]\n\n'; + context += '[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n' + + wrapUserText('style_hints', physicianMemories) + '\n[END STYLE HINTS]\n\n'; } var result = await callAI([ - { role: 'system', content: PROMPTS.sickVisitNote }, + { role: 'system', content: PROMPTS.sickVisitNote + INJECTION_GUARD }, { role: 'user', content: context } ], { model }); diff --git a/src/routes/wellVisit.js b/src/routes/wellVisit.js index b20f2bc..fd453ac 100644 --- a/src/routes/wellVisit.js +++ b/src/routes/wellVisit.js @@ -8,6 +8,7 @@ var { callAI } = require('../utils/ai'); var PROMPTS = require('../utils/prompts'); var { authMiddleware } = require('../middleware/auth'); var logger = require('../utils/logger'); +var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe'); // Load growth reference and BMI classification data var scheduleData; @@ -112,8 +113,8 @@ router.post('/well-visit/shadess', authMiddleware, async function(req, res) { } var result = await callAI([ - { role: 'system', content: PROMPTS.shadessAssessment }, - { role: 'user', content: inputText } + { role: 'system', content: PROMPTS.shadessAssessment + INJECTION_GUARD }, + { role: 'user', content: wrapUserText('sshadess_answers', inputText) } ], { model }); var dur = Date.now() - start; @@ -155,40 +156,20 @@ router.post('/well-visit/note', authMiddleware, async function(req, res) { var context = 'WELL CHILD VISIT\n'; context += 'Patient: ' + (patientAge || visitAge) + ', ' + (patientGender || 'Unknown') + '\n\n'; - if (vitals) { - context += 'VITAL SIGNS:\n' + vitals + '\n\n'; - } - if (measurements) { - context += 'MEASUREMENTS/GROWTH:\n' + measurements + '\n\n'; - } - if (parentConcerns) { - context += 'PARENT/PATIENT CONCERNS:\n' + parentConcerns + '\n\n'; - } - if (transcript) { - context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + transcript + '\n\n'; - } else if (dictation) { - context += 'PHYSICIAN DICTATION:\n' + dictation + '\n\n'; - } - if (screenings) { - context += 'SCREENINGS COMPLETED:\n' + screenings + '\n\n'; - } - if (vaccines) { - context += 'IMMUNIZATIONS TODAY:\n' + vaccines + '\n\n'; - } - if (shadessAssessment) { - context += 'SSHADESS PSYCHOSOCIAL ASSESSMENT:\n' + shadessAssessment + '\n\n'; - } - if (ros) { - context += ros + '\n\n'; - } - if (physicalExam) { - context += physicalExam + '\n\n'; - } - if (diagnoses) { - context += diagnoses + '\n\n'; - } + if (vitals) context += 'VITAL SIGNS:\n' + wrapUserText('vitals', vitals) + '\n\n'; + if (measurements) context += 'MEASUREMENTS/GROWTH:\n' + wrapUserText('measurements', measurements) + '\n\n'; + if (parentConcerns) context += 'PARENT/PATIENT CONCERNS:\n' + wrapUserText('concerns', parentConcerns) + '\n\n'; + if (transcript) context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + wrapUserText('transcript', transcript) + '\n\n'; + else if (dictation) context += 'PHYSICIAN DICTATION:\n' + wrapUserText('dictation', dictation) + '\n\n'; + if (screenings) context += 'SCREENINGS COMPLETED:\n' + wrapUserText('screenings', screenings) + '\n\n'; + if (vaccines) context += 'IMMUNIZATIONS TODAY:\n' + wrapUserText('vaccines', vaccines) + '\n\n'; + if (shadessAssessment) context += 'SSHADESS PSYCHOSOCIAL ASSESSMENT:\n' + wrapUserText('sshadess', shadessAssessment) + '\n\n'; + if (ros) context += wrapUserText('ros', ros) + '\n\n'; + if (physicalExam) context += wrapUserText('physical_exam', physicalExam) + '\n\n'; + if (diagnoses) context += wrapUserText('diagnoses', diagnoses) + '\n\n'; if (physicianMemories) { - context += '[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n' + physicianMemories + '\n[END STYLE HINTS]\n\n'; + context += '[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n' + + wrapUserText('style_hints', physicianMemories) + '\n[END STYLE HINTS]\n\n'; } // Add growth reference and feeding guidance for this age @@ -214,7 +195,7 @@ router.post('/well-visit/note', authMiddleware, async function(req, res) { var prompt = (noteStyle === 'short') ? PROMPTS.wellVisitShort : PROMPTS.wellVisitNote; var result = await callAI([ - { role: 'system', content: prompt }, + { role: 'system', content: prompt + INJECTION_GUARD }, { role: 'user', content: context } ], { model });