Prompt-injection wrap: remaining AI routes

Applied the <UNTRUSTED_*> 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 <UNTRUSTED_*>…</UNTRUSTED_*>
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).
This commit is contained in:
Daniel 2026-04-14 05:31:54 +02:00
parent 63f77aa9cf
commit ea03db3d45
5 changed files with 60 additions and 90 deletions

View file

@ -4,6 +4,7 @@ const { callAI } = require('../utils/ai');
const PROMPTS = require('../utils/prompts'); const PROMPTS = require('../utils/prompts');
const { authMiddleware } = require('../middleware/auth'); const { authMiddleware } = require('../middleware/auth');
var logger = require('../utils/logger'); var logger = require('../utils/logger');
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
// Generate chart review // Generate chart review
router.post('/generate-chart-review', authMiddleware, async (req, res) => { 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' }); 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'}`; 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 — // Use the top-level review type to select the prompt —
// the per-visit types only control data formatting, not 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 — // Include ALL visit data regardless of per-visit type —
// an outpatient chart review should include subspecialty consults too // an outpatient chart review should include subspecialty consults too
(visits || []).forEach(v => { (visits || []).forEach(v => {
clinicalData += `\n\n=== OUTPATIENT VISIT (${v.date}) ===\n${v.content}`; 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${v.labs}`; if (v.labs && v.labs.trim()) clinicalData += `\n--- Labs from this visit (${v.date}) ---\n` + wrapUserText('labs', v.labs);
}); });
(subspecialty || []).forEach(s => { (subspecialty || []).forEach(s => {
clinicalData += `\n\n=== SUBSPECIALTY: ${(s.specialty || 'Subspecialty').toUpperCase()}${s.specialistName || 'Unknown'} (${s.date}) ===\n${s.content}`; 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${s.labs}`; if (s.labs && s.labs.trim()) clinicalData += `\n--- Labs from this visit (${s.date}) ---\n` + wrapUserText('labs', s.labs);
}); });
(edVisits || []).forEach(v => { (edVisits || []).forEach(v => {
clinicalData += `\n\n=== ED VISIT (${v.date}) ===\n${v.content}`; 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${v.labs}`; if (v.labs && v.labs.trim()) clinicalData += `\n--- Labs from this visit (${v.date}) ---\n` + wrapUserText('labs', v.labs);
}); });
if (labs && labs.length > 0) { if (labs && labs.length > 0) {
clinicalData += '\n\n=== ADDITIONAL LABS (not tied to a specific visit) ==='; 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) { if (additionalInstructions) {
prompt += `\n\nADDITIONAL INSTRUCTIONS:\n${additionalInstructions}`; prompt += `\n\nADDITIONAL INSTRUCTIONS (operator-supplied, trusted):\n${additionalInstructions}`;
} }
const result = await callAI([ const result = await callAI([
{ role: 'system', content: prompt }, { role: 'system', content: prompt + INJECTION_GUARD },
{ role: 'user', content: clinicalData } { role: 'user', content: clinicalData }
], { model, maxTokens: 4000 }); ], { model, maxTokens: 4000 });

View file

@ -4,6 +4,7 @@ const { callAI } = require('../utils/ai');
const PROMPTS = require('../utils/prompts'); const PROMPTS = require('../utils/prompts');
const { authMiddleware } = require('../middleware/auth'); const { authMiddleware } = require('../middleware/auth');
var logger = require('../utils/logger'); var logger = require('../utils/logger');
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
// Generate hospital course // Generate hospital course
router.post('/generate-hospital-course', authMiddleware, async (req, res) => { 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 // Build the complete clinical data
let clinicalData = `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}`; 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 (setting) clinicalData += `\nSetting: ${setting}`;
if (los) clinicalData += `\nLength of Stay: ${los} days`; if (los) clinicalData += `\nLength of Stay: ${los} days`;
clinicalData += `\nFormat: ${format}`; clinicalData += `\nFormat: ${format}`;
if (edNote) { if (edNote) {
clinicalData += `\n\n=== ED NOTE (${edNote.date || 'date unknown'}) ===\n${edNote.content}`; clinicalData += `\n\n=== ED NOTE (${edNote.date || 'date unknown'}) ===\n` + wrapUserText('ed_note', edNote.content || '');
if (edNote.labs) clinicalData += `\nED Labs: ${edNote.labs}`; 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)); const sortedNotes = [...notes].sort((a, b) => new Date(a.date) - new Date(b.date));
sortedNotes.forEach((note, i) => { 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) { if (labs && labs.length > 0) {
clinicalData += '\n\n=== LABS ==='; clinicalData += '\n\n=== LABS ===';
labs.forEach(lab => { labs.forEach(lab => { clinicalData += `\n${lab.date}: ` + wrapUserText('labs', lab.values || ''); });
clinicalData += `\n${lab.date}: ${lab.values}`;
});
} }
if (additionalInstructions) { 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'
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]'; + wrapUserText('style_hints', physicianMemories) + '\n[END STYLE HINTS]';
const result = await callAI([ const result = await callAI([
{ role: 'system', content: prompt }, { role: 'system', content: prompt + INJECTION_GUARD },
{ role: 'user', content: clinicalData } { role: 'user', content: clinicalData }
], { model, maxTokens: 6000 }); ], { model, maxTokens: 6000 });
@ -107,8 +102,8 @@ router.post('/hospital-course-clarify', authMiddleware, async (req, res) => {
const { currentDraft, notes } = req.body; const { currentDraft, notes } = req.body;
const result = await callAI([ const result = await callAI([
{ role: 'system', content: PROMPTS.askClarification }, { role: 'system', content: PROMPTS.askClarification + INJECTION_GUARD },
{ role: 'user', content: `Current draft:\n${currentDraft}\n\nSource notes:\n${JSON.stringify(notes)}` } { role: 'user', content: `Current draft:\n` + wrapUserText('draft', currentDraft || '') + `\n\nSource notes:\n` + wrapUserText('notes', JSON.stringify(notes || [])) }
], { model: req.body.model }); ], { model: req.body.model });
res.json({ success: true, questions: result.content, model: result.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; const { currentCourse, updates, model, instructions } = req.body;
if (!currentCourse) return res.status(400).json({ error: 'No current course' }); if (!currentCourse) return res.status(400).json({ error: 'No current course' });
let userMsg = `CURRENT HOSPITAL COURSE:\n${currentCourse}\n\n`; let userMsg = `CURRENT HOSPITAL COURSE:\n` + wrapUserText('course', currentCourse) + `\n\n`;
if (updates) userMsg += `NEW UPDATES TO ADD:\n${updates}\n\n`; if (updates) userMsg += `NEW UPDATES TO ADD:\n` + wrapUserText('updates', updates) + `\n\n`;
if (instructions) userMsg += `INSTRUCTIONS:\n${instructions}`; if (instructions) userMsg += `INSTRUCTIONS:\n` + wrapUserText('instructions', instructions);
const result = await callAI([ const result = await callAI([
{ role: 'system', content: `You are updating a hospital course document with new information. { 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. Add the new information in the appropriate place. Maintain the same format and style.
If adding discharge day information, add it at the end. 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 } { role: 'user', content: userMsg }
], { model, maxTokens: 6000 }); ], { model, maxTokens: 6000 });

View file

@ -5,6 +5,7 @@ const PROMPTS = require('../utils/prompts');
const { authMiddleware } = require('../middleware/auth'); const { authMiddleware } = require('../middleware/auth');
const db = require('../db/database'); const db = require('../db/database');
var logger = require('../utils/logger'); var logger = require('../utils/logger');
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
// Get all milestones for client (authenticated users) // Get all milestones for client (authenticated users)
router.get('/milestones-data', authMiddleware, async (req, res) => { 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 prompt = (format === 'list') ? PROMPTS.milestoneList : PROMPTS.milestoneNarrative;
const result = await callAI([ const result = await callAI([
{ role: 'system', content: prompt }, { role: 'system', content: prompt + INJECTION_GUARD },
{ 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: '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 }); ], { model });
res.json({ 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' }); if (!narrative) return res.status(400).json({ error: 'No narrative' });
const result = await callAI([ const result = await callAI([
{ role: 'system', content: PROMPTS.milestoneSummary }, { role: 'system', content: PROMPTS.milestoneSummary + INJECTION_GUARD },
{ role: 'user', content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\nNARRATIVE:\n${narrative}` } { role: 'user', content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\nNARRATIVE:\n` + wrapUserText('narrative', narrative) }
], { model, maxTokens: 500 }); ], { model, maxTokens: 500 });
res.json({ success: true, summary: result.content, model: result.model }); res.json({ success: true, summary: result.content, model: result.model });

View file

@ -8,6 +8,7 @@ var { callAI } = require('../utils/ai');
var PROMPTS = require('../utils/prompts'); var PROMPTS = require('../utils/prompts');
var { authMiddleware } = require('../middleware/auth'); var { authMiddleware } = require('../middleware/auth');
var logger = require('../utils/logger'); var logger = require('../utils/logger');
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
// ── POST generate sick visit note ──────────────────────────────────────── // ── POST generate sick visit note ────────────────────────────────────────
router.post('/sick-visit/note', authMiddleware, async function(req, res) { 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 // Assemble context
var context = 'SICK VISIT\n'; var context = 'SICK VISIT\n';
context += 'Patient: ' + (patientAge || 'Unknown age') + ', ' + (patientGender || 'Unknown gender') + '\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) { if (transcript) {
context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + transcript + '\n\n'; context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + wrapUserText('transcript', transcript) + '\n\n';
} else if (dictation) { } else if (dictation) {
context += 'PHYSICIAN DICTATION:\n' + dictation + '\n\n'; context += 'PHYSICIAN DICTATION:\n' + wrapUserText('dictation', dictation) + '\n\n';
} }
if (ros) context += wrapUserText('ros', ros) + '\n\n';
if (ros) { if (physicalExam) context += wrapUserText('physical_exam', physicalExam) + '\n\n';
context += ros + '\n\n'; if (diagnoses) context += wrapUserText('diagnoses', diagnoses) + '\n\n';
}
if (physicalExam) {
context += physicalExam + '\n\n';
}
if (diagnoses) {
context += diagnoses + '\n\n';
}
if (physicianMemories) { 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([ var result = await callAI([
{ role: 'system', content: PROMPTS.sickVisitNote }, { role: 'system', content: PROMPTS.sickVisitNote + INJECTION_GUARD },
{ role: 'user', content: context } { role: 'user', content: context }
], { model }); ], { model });

View file

@ -8,6 +8,7 @@ var { callAI } = require('../utils/ai');
var PROMPTS = require('../utils/prompts'); var PROMPTS = require('../utils/prompts');
var { authMiddleware } = require('../middleware/auth'); var { authMiddleware } = require('../middleware/auth');
var logger = require('../utils/logger'); var logger = require('../utils/logger');
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
// Load growth reference and BMI classification data // Load growth reference and BMI classification data
var scheduleData; var scheduleData;
@ -112,8 +113,8 @@ router.post('/well-visit/shadess', authMiddleware, async function(req, res) {
} }
var result = await callAI([ var result = await callAI([
{ role: 'system', content: PROMPTS.shadessAssessment }, { role: 'system', content: PROMPTS.shadessAssessment + INJECTION_GUARD },
{ role: 'user', content: inputText } { role: 'user', content: wrapUserText('sshadess_answers', inputText) }
], { model }); ], { model });
var dur = Date.now() - start; 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'; var context = 'WELL CHILD VISIT\n';
context += 'Patient: ' + (patientAge || visitAge) + ', ' + (patientGender || 'Unknown') + '\n\n'; context += 'Patient: ' + (patientAge || visitAge) + ', ' + (patientGender || 'Unknown') + '\n\n';
if (vitals) { if (vitals) context += 'VITAL SIGNS:\n' + wrapUserText('vitals', vitals) + '\n\n';
context += 'VITAL SIGNS:\n' + 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 (measurements) { if (transcript) context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + wrapUserText('transcript', transcript) + '\n\n';
context += 'MEASUREMENTS/GROWTH:\n' + measurements + '\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 (parentConcerns) { if (vaccines) context += 'IMMUNIZATIONS TODAY:\n' + wrapUserText('vaccines', vaccines) + '\n\n';
context += 'PARENT/PATIENT CONCERNS:\n' + parentConcerns + '\n\n'; if (shadessAssessment) context += 'SSHADESS PSYCHOSOCIAL ASSESSMENT:\n' + wrapUserText('sshadess', shadessAssessment) + '\n\n';
} if (ros) context += wrapUserText('ros', ros) + '\n\n';
if (transcript) { if (physicalExam) context += wrapUserText('physical_exam', physicalExam) + '\n\n';
context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + transcript + '\n\n'; if (diagnoses) context += wrapUserText('diagnoses', diagnoses) + '\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 (physicianMemories) { 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 // 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 prompt = (noteStyle === 'short') ? PROMPTS.wellVisitShort : PROMPTS.wellVisitNote;
var result = await callAI([ var result = await callAI([
{ role: 'system', content: prompt }, { role: 'system', content: prompt + INJECTION_GUARD },
{ role: 'user', content: context } { role: 'user', content: context }
], { model }); ], { model });