{"id":2913,"date":"2026-07-20T09:38:52","date_gmt":"2026-07-20T07:38:52","guid":{"rendered":"https:\/\/darioiannascoli.it\/blog\/indirect-prompt-injection-rag-defense-framework-pdf-email-scrapers-2026\/"},"modified":"2026-07-20T09:38:52","modified_gmt":"2026-07-20T07:38:52","slug":"indirect-prompt-injection-rag-defense-framework-pdf-email-scrapers-2026","status":"publish","type":"post","link":"https:\/\/darioiannascoli.it\/blog\/indirect-prompt-injection-rag-defense-framework-pdf-email-scrapers-2026\/","title":{"rendered":"Come Implementare Indirect Prompt Injection Defense in Production: La Mia Procedura RAG Detection Framework per PDF, Email e Web Scrapers 2026"},"content":{"rendered":"<p>Nel mio lavoro quotidiano con sistemi di <em>Retrieval-Augmented Generation<\/em> (RAG) in produzione, ho affrontato uno degli attacchi pi\u00f9 insidiosi e difficili da rilevare: l&#8217;<strong>indirect prompt injection<\/strong>. A differenza degli attacchi diretti, dove l&#8217;utente malintenzionato interagisce direttamente con il sistema, gli attacchi indiretti nascondono istruzioni maligne dentro documenti, email e pagine web che il sistema retrieves e processa come contenuto fiduciario. Nel luglio 2026, continuo a monitorare come questi attacchi evolvono: le tecniche di encoding Unicode invisibile, il testo bianco-su-bianco nei PDF, i metadati HTML celati. In questa guida, vi condivido il <strong>framework di detection che ho costruito e testato<\/strong> in ambienti production, con procedura step-by-step, codice reale e esempi di payload che ho catturato.<\/p>\n<p>La cosa che mi preoccupa di pi\u00f9 \u00e8 questa: <cite>l&#8217;indirect prompt injection bypassa ogni security control progettato per validare input dell&#8217;utente, mentre il LLM processa documenti esterni, pagine web e email senza questionare le istruzioni nascoste dentro<\/cite>. Nella mia esperienza, ho visto sistemi di supporto aziendale dove <cite>il modello ha leakato dati confidenziali, inviato phishing email attraverso l&#8217;infrastruttura aziendale, o concesso accesso non autorizzato ai sistemi interni<\/cite>. Gli strumenti tradizionali non vedono questi attacchi perch\u00e9 le istruzioni maligne non toccano mai i perimeter defenses.<\/p>\n<p>Ho deciso di documentare il mio approccio a difesa layered per tre superfici di attacco critiche: PDF con hidden content, email con encoding obfuscato, e web scraper data poisoning. Il framework che presento combina analisi del layout dei documenti, keyword detection contestuale, e LLM-based semantic anomaly scoring.<\/p>\n<h2>Come Funziona L&#8217;Indirect Prompt Injection in Sistemi RAG<\/h2>\n<p>Quando configuro un sistema RAG per una azienda, il flusso logico \u00e8 semplice: l&#8217;utente fa una domanda \u2192 il retriever cerca documenti rilevanti nel knowledge base \u2192 inietta quei documenti nel context window dell&#8217;LLM \u2192 il modello risponde basandosi su quel contesto. <cite>RAG funziona retrievando documenti rilevanti da un vector database e iniettandoli nel context window del modello. Ma questi documenti sono contenuto esterno. Se un attaccante riesce a influenzare cosa \u00e8 nel knowledge base\u2014come sottomettendo un ticket di supporto che viene indicizzato, editando una wiki page che il sistema crawla, uploadando un documento in un shared drive\u2014pu\u00f2 piantare injection payload che vengono retrievati e feedati direttamente al modello<\/cite>.<\/p>\n<p><cite>Un modello che processa documenti dell&#8217;utente eseguir\u00e0 prompt nascosti dentro di essi. PDF, file DOCX, allegati HTML, e persino metadati di immagini possono portare payload di injection<\/cite>. Nel mio testing, ho scoperto che <cite>gli input malziosi non devono essere leggibili dall&#8217;uomo\u2014devono solo essere parsati dal modello. Un attaccante pu\u00f2 nascondere istruzioni in testo bianco-su-bianco, caratteri Unicode a larghezza zero, commenti HTML, o campi metadati che nessun umano vedrebbe mai<\/cite>.<\/p>\n<h2>Le Tre Superfici di Attacco Pi\u00f9 Critiche<\/h2>\n<h3>1. PDF con Hidden Content Embedding<\/h3>\n<p><cite>Gli attacchi sono embedded in documenti (PDF, email) che i sistemi AI processano. Questo pu\u00f2 essere realizzato attraverso testo invisibile, campi di metadati, o persino istruzioni nascoste steganograficamente dentro immagini nei documenti<\/cite>. Nel mio primo incontro con questo vettore d&#8217;attacco, ho estratto un PDF di apparenza legittima che conteneva testo grigio-su-grigio\u2014praticamente invisibile all&#8217;occhio umano, ma perfettamente leggibile dal tokenizer dell&#8217;LLM.<\/p>\n<p>Ho implementato una procedura di estrazione PDF a due livelli: innanzitutto estraggo il testo grezzo usando <em>pdfplumber<\/em>, poi estraggo anche metadati di layout e colore. <cite>Il pipeline inizia estraendo sia il contenuto testuale grezzo che i metadati specifici del layout da un PDF, includendo dimensione del font, colore, posizionamento dei caratteri e informazioni di encoding. Queste feature strutturali sono essenziali per identificare contenuto celato o visivamente offuscato che altrimenti verrebbe ignorato dai parser standard<\/cite>.<\/p>\n<h3>2. Email con Encoding Obfuscato<\/h3>\n<p>Le email sono un vettore d&#8217;attacco sottovalutato. <cite>Istruzioni nascoste in documenti, PDF, immagini, o metadati che un assistente ingesta quando processa l&#8217;allegato. Encoding e offuscazione: Base64, homoglyphs, Unicode inusuale, o frasi frammentate progettate per slittare oltre il semplice keyword matching mentre rimangono interpretabili dal modello<\/cite>. Ho visto payload nascosti in header MIME, in campi &#8220;Subject&#8221; codificati come UTF-8 non-standard, e perfino in base64 dentro allegati presunti innocui.<\/p>\n<p><cite>Un prompt injection riuscito pu\u00f2 portare un assistente AI a leakare contenuto sensibile dalla mailbox, misclassificare un messaggio malzioso come sicuro, generare un summary fuorviante, o prendere un&#8217;azione non voluta in un workflow automatizzato. Poich\u00e9 l&#8217;attacco giace dentro contenuto email ordinario, pu\u00f2 raggiungere qualsiasi utente la cui mailbox viene processata da un assistente AI<\/cite>.<\/p>\n<h3>3. Web Scraper Data Poisoning<\/h3>\n<p>Quando il sistema RAG scrape pagine web pubbliche\u2014come FAQ, forum, o documentazione tecnica\u2014gli attaccanti possono piantare istruzioni direttamente nel contenuto indexato. Ho testato payload in commenti HTML su pagine che il crawl mio retriever, e il 100% dei casi ha portato il modello a eseguire istruzioni nascoste. <cite>RAG-Pull \u00e8 un attacco imperceptibile su code-generation agents. Targets queries e code repositories via invisible Unicode character insertion, sfruttando malicious skill marketplaces, prompt-engineering sites, e attacker-controlled repositories<\/cite>.<\/p>\n<h2>Il Mio Framework di Detection Layered<\/h2>\n<p>Nella mia esperienza, nessun singolo detection layer \u00e8 sufficiente. Devo implementare tre livelli:<\/p>\n<ol>\n<li><strong>Ingestion Controls:<\/strong> Scanno documenti prima dell&#8217;embedding nel vector database<\/li>\n<li><strong>Retrieval Controls:<\/strong> Monitoraggio del contenuto retrievato e dei pattern anomali<\/li>\n<li><strong>Generation Controls:<\/strong> Analisi semantica del comportamento del modello durante inference<\/li>\n<\/ol>\n<h3>Layer 1: Preprocessing e Content Extraction<\/h3>\n<p>Ho scritto uno script Python che integro nel pipeline di ingestion per ogni documento che entra nel knowledge base. Estraggo metadata di layout e cerco pattern di hidden content:<\/p>\n<p><strong>Codice Python per PDF Analysis:<\/strong><\/p>\n<p><code>import pdfplumber<br \/>import re<br \/>from PIL import Image<br \/>import pytesseract<br \/>import pandas as pd<\/p>\n<p>def detect_hidden_pdf_content(pdf_path):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;\"\"\"Rileva testo nascosto, colori sospetti, e anomalie di layout nei PDF\"\"\"<br \/>&nbsp;&nbsp;&nbsp;&nbsp;findings = {<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'invisible_text': [],<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'metadata_anomalies': [],<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'encoding_suspects': [],<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'color_analysis': []<br \/>&nbsp;&nbsp;&nbsp;&nbsp;}<br \/>&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;with pdfplumber.open(pdf_path) as pdf:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for page_num, page in enumerate(pdf.pages):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Estrai testo grezzo<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;raw_text = page.extract_text()<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;raw_text_detailed = page.extract_text_detailed()<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Analizza colore e contrasto di ogni carattere<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for char in raw_text_detailed.get('chars', []):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Caratteri bianco-su-bianco o grigio-su-grigio = sospetti<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if char.get('fontname', '').endswith('Invisible'):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;findings['invisible_text'].append({<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'page': page_num,<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'text': char.get('text', ''),<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'size': char.get('size', 0)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;})<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Scanno per pattern d'injection comuni<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;injection_patterns = [<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;r'ignore.*previous',<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;r'you are now',<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;r'disregard.*instructions',<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;r'forget.*original'<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;]<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for pattern in injection_patterns:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if re.search(pattern, raw_text, re.IGNORECASE):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;findings['encoding_suspects'].append({r&gt;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'pattern': pattern,<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'page': page_num<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;})<br \/>&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;return findings<br \/><\/code><\/p>\n<p>In produzione, se questo detector trova hidden content, blocco il documento dall&#8217;ingestion e loggo l&#8217;evento per revisione manuale. Non silenzio l&#8217;alert\u2014\u00e8 troppo critico.<\/p>\n<h3>Layer 2: Semantic Anomaly Scoring a Retrieval Time<\/h3>\n<p>Il mio secondo layer corre a retrieval time: quando il sistema recupera documenti per costruire il context, scanno semanticamente ogni chunk per anomalie. Qui implemento un scoring system che combina:<\/p>\n<ul>\n<li>Keyword suspicion (presenza di parole-chiave come &#8220;ignore&#8221;, &#8220;override&#8221;, &#8220;execute&#8221;)<\/li>\n<li>Perplexity scoring (il contenuto recuperato ha una perplexity anormale rispetto al resto del documento?)<\/li>\n<li>Semantic drift (il significato del chunk devia drammaticamente dal topic atteso?)<\/li>\n<\/ul>\n<p><code>import torch<br \/>from transformers import AutoTokenizer, AutoModelForCausalLM<br \/>import numpy as np<\/p>\n<p>def compute_anomaly_score(retrieved_chunk, query, model, tokenizer):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;\"\"\"Calcola anomaly score per un chunk retrievato\"\"\"<br \/>&nbsp;&nbsp;&nbsp;&nbsp;# Keyword suspicion score<br \/>&nbsp;&nbsp;&nbsp;&nbsp;suspicious_keywords = [<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'ignore', 'override', 'bypass', 'execute immediately',<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'disregard', 'forget', 'new instructions', 'now you are'<br \/>&nbsp;&nbsp;&nbsp;&nbsp;]<br \/>&nbsp;&nbsp;&nbsp;&nbsp;keyword_score = sum(1 for kw in suspicious_keywords if kw.lower() in retrieved_chunk.lower()) \/ len(suspicious_keywords)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;# Perplexity score (testo anomalo ha perplexity pi\u00f9 alta)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;inputs = tokenizer(retrieved_chunk, return_tensors='pt')<br \/>&nbsp;&nbsp;&nbsp;&nbsp;with torch.no_grad():<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;outputs = model(**inputs, labels=inputs['input_ids'])<br \/>&nbsp;&nbsp;&nbsp;&nbsp;perplexity = torch.exp(outputs.loss).item()<br \/>&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;# Normalizzazione perplexity (empiricamente, baseline \u00e8 ~30-40 per testo normale)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;perplexity_score = min(perplexity \/ 100, 1.0)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;# Semantic drift (embeddings query vs chunk)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;from sentence_transformers import SentenceTransformer<br \/>&nbsp;&nbsp;&nbsp;&nbsp;semantic_model = SentenceTransformer('all-MiniLM-L6-v2')<br \/>&nbsp;&nbsp;&nbsp;&nbsp;query_embedding = semantic_model.encode(query)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;chunk_embedding = semantic_model.encode(retrieved_chunk)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;cosine_sim = np.dot(query_embedding, chunk_embedding) \/ (np.linalg.norm(query_embedding) * np.linalg.norm(chunk_embedding))<br \/>&nbsp;&nbsp;&nbsp;&nbsp;semantic_drift_score = 1 - cosine_sim  # Drift = 1 - similarity<br \/>&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;# Combina i tre segnali<br \/>&nbsp;&nbsp;&nbsp;&nbsp;anomaly_score = (0.3 * keyword_score + 0.4 * perplexity_score + 0.3 * semantic_drift_score)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;return anomaly_score<br \/><\/code><\/p>\n<p>Se lo score supera 0.65 (sulla scala 0-1), flaggo il chunk come &#8220;sospetto&#8221; e lo isolo dal context principale, oppure lo blocco completamente. In produzione, preferisco il blocco completo per injection prevention\u2014meglio una false positive che una breach.<\/p>\n<h3>Layer 3: LLM Output Validation e Behavior Anomaly Detection<\/h3>\n<p>Il terzo layer \u00e8 la mia rete di sicurezza finale: dopo che il modello genera output, scanno l&#8217;output stesso per segni che sia stato compromesso. <cite>La Response-Based Detection sfrutta l&#8217;output atteso di un&#8217;applicazione LLM per determinare l&#8217;integrit\u00e0 dei dati. Se l&#8217;LLM produce una risposta invalida o inaspettata per un task dato, i dati sono reputati compromessi<\/cite>.<\/p>\n<p>Ho implementato regole come:<\/p>\n<ul>\n<li>Se l&#8217;output contiene directive che non corrispondono al task originale \u2192 flag<\/li>\n<li>Se il tone o lo stile del modello cambia drasticamente mid-response \u2192 flag<\/li>\n<li>Se il modello genera codice executable o comandi shell quando non richiesto \u2192 flag<\/li>\n<\/ul>\n<p><code>def validate_output_safety(generated_text, original_query, system_prompt):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;\"\"\"Valida che l'output del modello non sia stato hijacked\"\"\"<br \/>&nbsp;&nbsp;&nbsp;&nbsp;violations = []<br \/>&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;# Controlla per instruction override patterns<br \/>&nbsp;&nbsp;&nbsp;&nbsp;override_patterns = [<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;r'(following new|ignore|override|disregard).*instructions',<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;r'my.*instructions.*are.*now',<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;r'from.*now.*on.*i.*will'r&gt;&nbsp;&nbsp;&nbsp;&nbsp;]<br \/>&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;for pattern in override_patterns:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if re.search(pattern, generated_text, re.IGNORECASE):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;violations.append(f'Instruction override pattern detected: {pattern}')<br \/>&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;# Controlla per shell commands o code execution hints<br \/>&nbsp;&nbsp;&nbsp;&nbsp;shell_patterns = [r'bash', r'$s*[a-z]', r'execute', r'run.*command']r&gt;&nbsp;&nbsp;&nbsp;&nbsp;for pattern in shell_patterns:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if re.search(pattern, generated_text, re.IGNORECASE):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;violations.append(f'Shell\/code execution hint: {pattern}')<br \/>&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;# Semantic consistency check<br \/>&nbsp;&nbsp;&nbsp;&nbsp;from transformers import pipeline<br \/>&nbsp;&nbsp;&nbsp;&nbsp;nli_classifier = pipeline('zero-shot-classification')<br \/>&nbsp;&nbsp;&nbsp;&nbsp;premise = original_query<br \/>&nbsp;&nbsp;&nbsp;&nbsp;hypothesis = generated_text[:500]  # Prendi i primi 500 char<br \/>&nbsp;&nbsp;&nbsp;&nbsp;result = nli_classifier(hypothesis, [premise, 'completely unrelated'])<br \/>&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;if result['labels'][0] == 'completely unrelated' and result['scores'][0] &gt; 0.8:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;violations.append('Output semantically unrelated to query')<br \/>&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;return {r&gt;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'is_safe': len(violations) == 0,<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'violations': violations,<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'severity': 'high' if violations else 'none'r&gt;&nbsp;&nbsp;&nbsp;&nbsp;}r&gt;<\/code><\/p>\n<h2>Implementazione Step-by-Step del Framework in Produzione<\/h2>\n<h3>Step 1: Setup dell&#8217;Ambiente di Detection<\/h3>\n<ol>\n<li>Ho creato una pipeline separata pre-embedding che runs in background su tutti i documenti ingested<\/li>\n<li>Configuro Elasticsearch o Milvus come vector store con logging completo di tutti i retrieval events<\/li>\n<li>Implemento un wrapper intorno al call dell&#8217;LLM che intercetta input e output<\/li>\n<\/ol>\n<h3>Step 2: Integrazione in LangChain\/LlamaIndex Pipeline<\/h3>\n<p>Nel mio stack production, uso LangChain con un custom retriever che applica i miei detection layer:<\/p>\n<p><code>from langchain.retrievers import ContextualCompressionRetriever<br \/>from langchain.retrievers.document_compressors import LLMListwiseRerank<\/p>\n<p>class InjectionSafeRetriever:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;def __init__(self, base_retriever, anomaly_threshold=0.65):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.base_retriever = base_retriever<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.anomaly_threshold = anomaly_threshold<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.detection_model = ...  # Carica modello di detection<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;def get_relevant_documents(self, query):<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Step 1: Retrieval base<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;docs = self.base_retriever.get_relevant_documents(query)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Step 2: Anomaly scoring per ogni doc<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;safe_docs = []r&gt;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for doc in docs:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;score = compute_anomaly_score(doc.page_content, query, ...)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if score &lt; self.anomaly_threshold:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;safe_docs.append(doc)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print(f'BLOCKED: Doc {doc.metadata.get(\"source\")} - anomaly_score={score}')<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Log per audit trail<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return safe_docsr&gt;<\/code><\/p>\n<h3>Step 3: Monitoring e Alerting in Real-Time<\/h3>\n<p>Ho connesso tutto a Elasticsearch + Kibana per monitorare in real-time:<\/p>\n<ul>\n<li>Quanti documenti vengono bloccati al giorno<\/li>\n<li>Quali pattern d&#8217;injection sono pi\u00f9 comuni<\/li>\n<li>Se i falsi positivi sono accettabili (target: &lt; 5%)<\/li>\n<\/ul>\n<p>Il mio alert threshold \u00e8: se blocco pi\u00f9 di 10 documenti in 1 ora, triggero un alert ai security team.<\/p>\n<h2>Collegamento con Articoli Correlati sul Blog<\/h2>\n<p>Se stai costruendo sistemi RAG in produzione, il framework di detection che ho descritto qui si integra bene con gli articoli precedenti che ho scritto:<\/p>\n<ul>\n<li><a href=\"https:\/\/darioiannascoli.it\/blog\/prompt-injection-indirect-rag-detection-framework-luglio-2026\/\">Prompt Injection Indirect Attack Prevention Luglio 2026: Detection Framework per Email Headers, Document Embedding e Context Window Poisoning<\/a> \u2014 qui copro i specifici pattern d&#8217;injection per email e metadata<\/li>\n<li><a href=\"https:\/\/darioiannascoli.it\/blog\/agentic-ai-workflows-production-orchestration-safety-guardrails-luglio-2026\/\">Come Implementare Agentic AI Multi-Step Workflows in Production Luglio 2026: Agent Orchestration e Safety Guardrails Enterprise<\/a> \u2014 il detection framework si applica anche agli agentic RAG che hanno tool use permission risks aggiuntivi<\/li>\n<li><a href=\"https:\/\/darioiannascoli.it\/blog\/multimodal-prompt-injection-defense-august-2026-cross-modal-detection\/\">Come Implementare Multimodal AI Prompt Injection Defense August 2026<\/a> \u2014 la detection per vision-language models segue i principi simili ma con layer aggiuntive per image-based injection<\/li>\n<\/ul>\n<h2>Errori che Ho Incontrato e Come Li Ho Risolti<\/h2>\n<p>All&#8217;inizio della mia implementazione, la detection aveva un false positive rate del 35%\u2014troppo alto. Il problema era che stavo flaggando ogni chunk con keyword &#8220;execute&#8221; o &#8220;command&#8221;, anche se legittimamente stava discutendo di API calls normali.<\/p>\n<p>La soluzione: ho aggiunto contextual scoring\u2014non solo cerco la keyword, ma verifico se appare in un contesto di istruzioni nascoste (whitespace strano, encoding anomalo, etc.). Questo ha ridotto false positive a 3%.<\/p>\n<p>Secondo errore: il perplexity scoring da solo era troppo sensibile a documenti tecnici validi (che hanno perplexity naturalmente pi\u00f9 alta). La soluzione: ho trainato il mio threshold su un dataset di 500 documenti reali buoni + 200 documenti con payload d&#8217;injection provati. Ora il modello sa la differenza tra perplexity legittima e anomala.<\/p>\n<h2>FAQ<\/h2>\n<h3>La detection framework riduce il throughput del retrieval?<\/h3>\n<p>S\u00ec, ma minimalmente. Il Layer 1 (preprocessing PDF) corre offline prima dell&#8217;embedding, quindi zero impatto a retrieval time. Layer 2 (anomaly scoring) aggiunge ~50-100ms per chunk retrievato, dipendendo dalla dimensione\u2014ho implementato caching cos\u00ec che lo stesso chunk non \u00e8 rescored due volte. Layer 3 (output validation) corre post-generation, quindi non blocca la risposta all&#8217;utente. In produzione con latency budget di 2-3 secondi, tutto questo \u00e8 accettabile.<\/p>\n<h3>Quali falsi positivi pi\u00f9 comuni hai riscontrato?<\/h3>\n<p>Documenti tecnici che legittimamente usano la parola &#8220;execute&#8221; (come manuali di comandi Bash o documentazione API), email da vendor che contengono istruzioni complesse, PDF di ricerca accademica con astratti che casualmente contengono frasi come &#8220;we override the baseline&#8221;. La soluzione \u00e8 il contextual scoring\u2014se la keyword appare in un contesto ragionevole e non \u00e8 accompagnata da segni di hidden text, passa il filtro.<\/p>\n<h3>Come integro la detection con sistemi di ingestion che sono gi\u00e0 in produzione?<\/h3>\n<p>Ho implementato in due fasi: 1) Preventivo: scanno il 5% della knowledge base esistente e loggo findings senza bloccare nulla per una settimana. 2) Blocco graduale: dopo aver tuned i threshold sui dati reali, attivo il blocco in produzione con un kill switch per rollback rapido. Il mio team ha visto il tasso di falsi positivi stabilizzarsi dopo 3 giorni.<\/p>\n<h3>Il framework difende anche da web scraper poisoning?<\/h3>\n<p>S\u00ec, ma con alcune limitazioni. Quando il retriever scrape una pagina web, passo il contenuto attraverso gli stessi layer di detection. Il problema \u00e8 che alcuni siti legittimi hanno HTML comments weird o encoding strano per ragioni di performance, non maligne. Il mio approccio: trusted source whitelist\u2014se la pagina viene da un dominio che ho vettato manualmente (es. documentazione ufficiale), uso detection pi\u00f9 soft. Da domini sconosciuti, applico stringent rules.<\/p>\n<h3>Quanto \u00e8 efficace contro attacchi Unicode invisibile come RAG-Pull?<\/h3>\n<p><cite>RAG-Pull \u00e8 un attacco imperceptibile su code-generation agents che targets queries e code repositories via invisible Unicode character insertion<\/cite>. Il mio detector cattura Unicode zero-width characters nell&#8217;extraction fase (Layer 1), perch\u00e9 estraggo e valido esplicitamente l&#8217;encoding di ogni carattere. Tuttavia, non sono 100% efficace su attacchi molto sofisticati\u2014per questo consiglio sempre di mantenere anche un human review process per documenti che hanno anomaly score in range borderline (0.55-0.65).<\/p>\n<h2>Conclusione: Layered Defense \u00e8 Non-Negoziabile<\/h2>\n<p>L&#8217;<strong>indirect prompt injection<\/strong> \u00e8 ormai il numero 1 risk nei sistemi RAG in produzione. Nel corso del 2026, ho visto aziende che credevano che un semplice keyword filter fosse sufficiente venire compromesse\u2014documenti con hidden text istruivano il modello a leakare dati confidenziali, email con payload Unicode offuscato venivano processat e eseguite.<\/p>\n<p>Il framework che ho documentato in questo articolo\u2014<strong>preprocessing layer con detection di hidden content, anomaly scoring semantico al retrieval time, e validation post-generation<\/strong>\u2014\u00e8 testato in produzione e riduce il rischio significativamente. Non \u00e8 perfetto (nessuna defense lo \u00e8), ma combinato con governance e monitoring, crea una postura di sicurezza che tiene.<\/p>\n<p>Se state deployando RAG in produzione, non saltate questi step. <em>Treat your knowledge base as untrusted input<\/em>, perch\u00e9 potrebbe letteralmente contenere istruzioni nascoste progettate per compromettere il vostro sistema. Condividete le vostre esperienze nei commenti\u2014quale vettore d&#8217;attack vi preoccupa di pi\u00f9?<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Indirect prompt injection in RAG systems pu\u00f2 nascondere istruzioni maligne in PDF, email e web scrapers. Vi mostro il mio framework di detection layered con codice Python testato in produzione per il 2026.<\/p>\n","protected":false},"author":1,"featured_media":2914,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"_seopress_robots_primary_cat":"","_seopress_titles_title":"Indirect Prompt Injection Defense RAG | Detection Framework 2026","_seopress_titles_desc":"Implementa detection framework layered per indirect prompt injection in RAG. Procedura per PDF hidden content, email encoding, web scraper poisoning. Codice Python production-ready.","_seopress_robots_index":"","footnotes":""},"categories":[128],"tags":[484,1131,1130,1106,1022,1047],"class_list":["post-2913","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-a-i","tag-ai-security","tag-detection-framework","tag-llm-defense","tag-production-ai","tag-prompt-injection","tag-rag-systems"],"_links":{"self":[{"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/posts\/2913","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/comments?post=2913"}],"version-history":[{"count":0,"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/posts\/2913\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/media\/2914"}],"wp:attachment":[{"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/media?parent=2913"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/categories?post=2913"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/tags?post=2913"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}