Back to skill

Security audit

QDrant Ingestion & Retrieval Best Practices

Security checks for vulnerabilities and agentic risk

Overview

This is a documentation-only Qdrant RAG skill, but its production code examples include under-scoped data writes/deletes and classification paths that could weaken tenant isolation or access boundaries if copied as-is.

Treat this skill as requiring review before installation. It is not an executable backdoor, but do not copy the ingestion and migration snippets into production without tenant-qualified IDs and filters, internally derived collection placement, security-metadata change detection, privacy-aware logging, and explicit safeguards for delete/migration operations.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Error
Location
guides/07-ingestion-pipeline.md:78
Finding

Tenant-unqualified point identities and filters permit cross-tenant data modification

Content
View full analysis
str: """Generate a stable UUID from a doc_id string (deterministic).""" return str(uuid.uuid5(uuid.NAMESPACE_DNS, doc_id)) ``` The related lookup and mutation methods also omit tenant identifiers: ```python def _is_duplicate_chunk(self, content_hash: str, source_record_id: str, collection: str) -> bool: """Check if a chunk with this content_hash already exists for this source record.""" results = self.client.scroll( collection_name=collection, scroll_filter=models.Filter( must=[ models.FieldCondition(key="content_hash", match=models.MatchValue(value=content_hash)), models.FieldCondition(key="source_record_id", match=models.MatchValue(value=source_record_id)), ] ), limit=1, ) return len(results[0]) > 0 def _refresh_metadata_only(self, source_metadata: dict, collection: str): """For unchanged content, update only the ingested_at and source_last_modified timestamps.""" now = datetime.now(timezone.utc).isoformat() self.client.set_payload( collection_name=collection, payload={ "ingested_at": now, "source_last_modified": source_metadata.get("source_last_modified", now), "ingestion_run_id": self.run_id, }, points=models.Filter( must=[ models.FieldCondition( key="source_record_id", mat ...[truncated 3643 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
guides/07-ingestion-pipeline.md:25
Finding

Caller-controlled collection placement and text-only change detection can bypass sensitivity reclassification

Content
View full analysis
dict: """ Full pipeline for a single source document. Returns a summary of what was done. """ # Step 1: Normalization normalized_text = normalizer.normalize(source_content, source_metadata) # Step 2: Document-level hash (change detection) doc_hash = hashlib.sha256(normalized_text.encode("utf-8")).hexdigest() # Step 3: Change detection — check if we've seen this document before existing_hash = self._get_existing_hash( source_metadata["source_system"], source_metadata["source_record_id"], collection_name, ) if existing_hash == doc_hash: # Content unchanged — only refresh metadata timestamps self._refresh_metadata_only(source_metadata, collection_name) return {"status": "unchanged", "doc_hash": doc_hash} # Step 4: Classification sensitivity = classifier.classify(normalized_text, source_metadata) ``` The classified sensitivity is stored in the payload, but it is not used to select or validate the destination collection: ```python payload = self._build_payload( source_metadata=source_metadata, doc_id=doc_id, chunk_text=chunk_text, chunk_index=i, chunk_total=new_total, chunk_hash=chunk_hash, doc_hash=doc_hash, sensitivity=sensitivity, data_scope_tags=data_scope_tags, chunk_strategy=chunker.strategy_name, ) self.client.upsert( collection_name=collection_name, points=points, wait=True, ) ``` ### Technical Analysis The project treats collection placement as a hard security boundary: - `pub ...[truncated 3171 chars]
Remediation
View remediation
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (11)

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The skill description includes very broad triggers such as 'any mention of Qdrant, RAG pipeline, vector ingestion, chunking, embeddings, hybrid search, payload filters, or access-controlled retrieval,' which can cause the skill to activate in many loosely related contexts. Over-broad invocation increases the chance that an agent loads and follows irrelevant instructions, expanding attack surface and creating opportunities for prompt/instruction interference or unintended data handling in unrelated tasks.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

This markdown file describes a workflow step to delete existing chunks from a prior collection, which is a destructive operation that can affect stored data integrity. The surrounding guidance explains why reclassification is needed, but it does not warn readers about the risk of accidental data loss or recommend confirmation, backup, or rollback precautions.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

Line L035 instructs normalizing user references to a canonical directory display name across content. This imposes an organizational naming/identity convention in natural language without documenting user opt-in, alternatives, or a clearly justified region/compliance constraint.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

This markdown file includes code examples that call OpenAI's embeddings API with user-provided text inputs, which transmits content off-system to a third-party provider. The surrounding guide describes model selection and usage but does not warn readers that using these examples sends document/query data externally and may have privacy or compliance implications.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The guide explicitly instructs hard-deleting chunks on delete and access-revoked events, which is a destructive and potentially irreversible operation affecting stored user data. In this markdown file, there is no accompanying warning or cautionary note about the impact, recovery limitations, or need for operator confirmation.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The markdown includes example code that copies points to a new collection and then deletes them from the old collection, which can cause irreversible data loss if migration is incomplete or misconfigured. The surrounding documentation does not provide a clear warning about validating migration success, rollback planning, or the destructive nature of the delete step.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The guide’s deduplication section says duplicates should be identified using content_hash and parent_doc_id, but the implementation actually uses content_hash and source_record_id. This inconsistency can cause incorrect deduplication behavior, leading to missed updates or unintended retention of duplicate chunks within a parent document hierarchy, which can undermine data integrity and retrieval correctness.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The observability example logs raw query text and returned document IDs, which can expose sensitive user prompts, confidential search topics, and identifiers for internal documents. If logs are broadly accessible, retained too long, or forwarded to third-party telemetry systems, they can become a secondary data leakage channel even when retrieval access controls are otherwise correct.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
86% confidence
Finding

This markdown file describes operational procedures including deleting expired chunks and modifying collection payloads, which can affect user data and system state. While the code snippets mention what they do, the document does not provide an explicit caution or warning to operators about irreversible deletion, validating scope first, or using non-production testing before execution.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
86% confidence
Finding

This markdown guide instructs implementers to delete out-of-range chunks from Qdrant during re-ingestion, which is a destructive data operation. The surrounding documentation does not include any warning, confirmation expectation, or cautionary note about verifying the new chunk count before deletion.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
76% confidence
Finding

The prose at L163 states as an absolute rule that 'The AI agent never constructs these filters.' Immediately after, the guide provides a reusable build_access_filter function that constructs those filters from a dictionary input. While this may be intended for orchestration-layer code, the documentation as written blurs that boundary and contradicts the categorical claim.

Content

No source excerpt is available for this finding.