Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# Framework-based controllers
from .controllers.prompt_injection_controller import PromptInjectionController
from .controllers.indirect_prompt_injection_controller import IndirectPromptInjectionController
from .controllers.bola_chatbot_controller import BOLAChatbotController
from .framework import register_controllers

app = FastAPI(title="LLMForge Prompt Injection Lab")
Expand Down
131 changes: 131 additions & 0 deletions src/controllers/bola_chatbot_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
LLM Orchestrated BOLA Controller - Framework Version

Uses the decorator-driven framework to define BOLA (Broken Object Level Access) vulnerability levels.
Demonstrates how an LLM-powered chatbot can be exploited with malicious prompts to access other users' data.
"""

from fastapi import Request
import httpx

from ..framework import (
vulnerable_llm_controller,
vulnerable_llm_endpoint,
attack_vector,
Variant,
VulnerabilityType,
)
from ..service.vulnerabilities.bola_chatbot_lab import evaluate_level


@vulnerable_llm_controller(
name="bola_chatbot",
description="LLM Orchestrated BOLA (Broken Object Level Access)",
)
class BOLAChatbotController:
"""LLM Orchestrated BOLA vulnerability levels - Medical Chatbot."""

@vulnerable_llm_endpoint(
level="level_1",
variant=Variant.UNSECURE,
html_template="bola/bola_chatbot_level1",
method="POST"
)
@attack_vector(
vulnerability_exposed=[VulnerabilityType.BOLA],
description="attack.direct_patient_id_request",
payload="payload.l1_direct_request"
)
@attack_vector(
vulnerability_exposed=[VulnerabilityType.BOLA],
description="attack.patient_id_injection",
payload="payload.l1_id_injection"
)
async def level1(self, request: Request) -> dict:
"""Level 1: No Access Controls - LLM can freely choose any patient ID"""
data = await request.json()
user_input = data.get("user_input", "")
model = data.get("model")

try:
# In Level 1, LLM orchestrates which patient to access
# current_patient_id is just a placeholder, LLM can override it
return await evaluate_level(
1,
user_input,
current_patient_id="patient_001",
model=model
)
except ValueError as exc:
return {"error": str(exc)}
except httpx.RequestError as exc:
return {"error": "Model service unavailable"}

@vulnerable_llm_endpoint(
level="level_2",
variant=Variant.UNSECURE,
html_template="bola/bola_chatbot_level2",
method="POST"
)
@attack_vector(
vulnerability_exposed=[VulnerabilityType.BOLA],
description="attack.prompt_injection_bypass",
payload="payload.l2_prompt_injection"
)
@attack_vector(
vulnerability_exposed=[VulnerabilityType.BOLA],
description="attack.context_confusion",
payload="payload.l2_context_confusion"
)
async def level2(self, request: Request) -> dict:
"""Level 2: Prompt-Level Guard Rails - System prompt restricts to current patient but bypassable"""
data = await request.json()
user_input = data.get("user_input", "")
model = data.get("model")

try:
# Level 2: Current patient is specified in system prompt but LLM can override via prompt injection
return await evaluate_level(
2,
user_input,
current_patient_id="patient_001",
model=model
)
except ValueError as exc:
return {"error": str(exc)}
except httpx.RequestError as exc:
return {"error": "Model service unavailable"}

@vulnerable_llm_endpoint(
level="level_3",
variant=Variant.SECURE,
html_template="bola/bola_chatbot_level3",
method="POST"
)
@attack_vector(
vulnerability_exposed=[VulnerabilityType.BOLA],
description="attack.application_layer_protected",
payload="payload.l3_application_controls"
)
async def level3(self, request: Request) -> dict:
"""Level 3: Application-Layer Guard Rails - Backend enforces patient ID"""
data = await request.json()
user_input = data.get("user_input", "")
model = data.get("model")

try:
# Level 3: Patient ID is hardcoded by backend (like from authentication/session cookie)
# LLM cannot override this - it comes from backend authorization layer
# In a real implementation, this would come from request.user.patient_id or session cookie
authenticated_patient_id = "patient_001" # Hardcoded here since no auth system shown

return await evaluate_level(
3,
user_input,
current_patient_id=authenticated_patient_id,
model=model
)
except ValueError as exc:
return {"error": str(exc)}
except httpx.RequestError as exc:
return {"error": "Model service unavailable"}
8 changes: 4 additions & 4 deletions src/controllers/indirect_prompt_injection_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class IndirectPromptInjectionController:
@vulnerable_llm_endpoint(
level="level_1",
variant=Variant.UNSECURE,
html_template="indirect_prompt_injection_level1",
html_template="indirect_prompt_injection_template",
method="POST",
secret_token="ind_l1_A9rQ2mX7tP4kV8"
)
Expand Down Expand Up @@ -66,7 +66,7 @@ async def level1(self, request: Request) -> dict:
@vulnerable_llm_endpoint(
level="level_2",
variant=Variant.UNSECURE,
html_template="indirect_prompt_injection_level2",
html_template="indirect_prompt_injection_template",
method="POST",
secret_token="ind_l2_N6wC3zR1yH8dF5"
)
Expand Down Expand Up @@ -103,7 +103,7 @@ async def level2(self, request: Request) -> dict:
@vulnerable_llm_endpoint(
level="level_3",
variant=Variant.UNSECURE,
html_template="indirect_prompt_injection_level3",
html_template="indirect_prompt_injection_template",
method="POST",
secret_token="ind_l3_T4vM9qK2pS7xB1"
)
Expand Down Expand Up @@ -140,7 +140,7 @@ async def level3(self, request: Request) -> dict:
@vulnerable_llm_endpoint(
level="level_4",
variant=Variant.SECURE,
html_template="indirect_prompt_injection_level4",
html_template="indirect_prompt_injection_template",
method="POST",
secret_token=None
)
Expand Down
20 changes: 10 additions & 10 deletions src/controllers/prompt_injection_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class PromptInjectionController:
@vulnerable_llm_endpoint(
level="level_1",
variant=Variant.UNSECURE,
html_template="prompt_injection_level1",
html_template="prompt_injection_template",
method="POST",
secret_token="pi_l1_C9vT2mQ7xL4rN8kD"
)
Expand Down Expand Up @@ -61,7 +61,7 @@ async def level1(self, request: Request) -> dict:
@vulnerable_llm_endpoint(
level="level_2",
variant=Variant.UNSECURE,
html_template="prompt_injection_level2",
html_template="prompt_injection_template",
method="POST",
secret_token="pi_l2_R5nW8zK1uP3aX6hM"
)
Expand Down Expand Up @@ -92,7 +92,7 @@ async def level2(self, request: Request) -> dict:
@vulnerable_llm_endpoint(
level="level_3",
variant=Variant.UNSECURE,
html_template="prompt_injection_level3",
html_template="prompt_injection_template",
method="POST",
secret_token="pi_l3_J4qN7sV2yB9tD6pL"
)
Expand Down Expand Up @@ -123,7 +123,7 @@ async def level3(self, request: Request) -> dict:
@vulnerable_llm_endpoint(
level="level_4",
variant=Variant.UNSECURE,
html_template="prompt_injection_level4",
html_template="prompt_injection_template",
method="POST",
secret_token="pi_l4_M8xP3dR6kT1vQ9nS"
)
Expand Down Expand Up @@ -154,7 +154,7 @@ async def level4(self, request: Request) -> dict:
@vulnerable_llm_endpoint(
level="level_5",
variant=Variant.UNSECURE,
html_template="prompt_injection_level5",
html_template="prompt_injection_template",
method="POST",
secret_token="pi_l5_T2kV9mC4qH7xR1dN"
)
Expand Down Expand Up @@ -185,7 +185,7 @@ async def level5(self, request: Request) -> dict:
@vulnerable_llm_endpoint(
level="level_6",
variant=Variant.UNSECURE,
html_template="prompt_injection_level6",
html_template="prompt_injection_template",
method="POST",
secret_token="pi_l6_P7rD1wN5zK8mQ3tV"
)
Expand Down Expand Up @@ -216,7 +216,7 @@ async def level6(self, request: Request) -> dict:
@vulnerable_llm_endpoint(
level="level_7",
variant=Variant.UNSECURE,
html_template="prompt_injection_level7",
html_template="prompt_injection_template",
method="POST",
secret_token="pi_l7_X3nT8qL6vR2mK9dP"
)
Expand Down Expand Up @@ -247,7 +247,7 @@ async def level7(self, request: Request) -> dict:
@vulnerable_llm_endpoint(
level="level_8",
variant=Variant.UNSECURE,
html_template="prompt_injection_level8",
html_template="prompt_injection_template",
method="POST",
secret_token="pi_l8_V6mQ2rT9kD4xN7pW"
)
Expand Down Expand Up @@ -278,7 +278,7 @@ async def level8(self, request: Request) -> dict:
@vulnerable_llm_endpoint(
level="level_9",
variant=Variant.UNSECURE,
html_template="prompt_injection_level9",
html_template="prompt_injection_template",
method="POST",
secret_token="pi_l9_K3xR8nP5qT2mV6dL"
)
Expand Down Expand Up @@ -309,7 +309,7 @@ async def level9(self, request: Request) -> dict:
@vulnerable_llm_endpoint(
level="level_10",
variant=Variant.SECURE,
html_template="prompt_injection_level10",
html_template="prompt_injection_template",
method="POST",
secret_token=None
)
Expand Down
1 change: 1 addition & 0 deletions src/framework/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class VulnerabilityType(Enum):
INDIRECT_PROMPT_INJECTION = (None, None, "Indirect Prompt Injection")
DELIMITER_CONFUSION = (None, None, "Delimiter Confusion")
JSON_INJECTION = (None, None, "JSON Injection")
BOLA = (None, None, "Broken Object Level Authorization (BOLA) in LLMs")

def __init__(self, cwe_id: Optional[int], wasc_id: Optional[int], custom: Optional[str] = None) -> None:
self.cwe_id = cwe_id
Expand Down
40 changes: 20 additions & 20 deletions src/framework/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,29 +269,29 @@ def get_facade_vulnerability_definitions(controllers: Optional[List[Type]] = Non
for endpoint in controller_metadata:
controller_name = str(endpoint.get("name", ""))
display_name = "".join(part.capitalize() for part in controller_name.split("_"))
template_base = "indirect_prompt_injection_template" if controller_name == "indirect_prompt_injection" else "prompt_injection_template"
resource_information = {
"htmlResource": {
"resourceType": "HTML",
"isAbsolute": False,
"uri": f"{APP_BASE_PATH}/static/facade/{template_base}.html",
},
"staticResources": [
{
"resourceType": "CSS",
"isAbsolute": False,
"uri": f"{APP_BASE_PATH}/static/facade/{template_base}.css",
},
{
"resourceType": "JAVASCRIPT",
"isAbsolute": False,
"uri": f"{APP_BASE_PATH}/static/facade/{template_base}.js",
},
],
}

levels = []
for level in endpoint.get("levels", []):
template_base = level.get("html_template")
resource_information = {
"htmlResource": {
"resourceType": "HTML",
"isAbsolute": False,
"uri": f"{APP_BASE_PATH}/static/facade/{template_base}.html",
},
"staticResources": [
{
"resourceType": "CSS",
"isAbsolute": False,
"uri": f"{APP_BASE_PATH}/static/facade/{template_base}.css",
},
{
"resourceType": "JAVASCRIPT",
"isAbsolute": False,
"uri": f"{APP_BASE_PATH}/static/facade/{template_base}.js",
},
],
}
levels.append({
"levelIdentifier": level.get("level", "unknown"),
"variant": getattr(level.get("variant"), "value", level.get("variant", "UNSECURE")),
Expand Down
12 changes: 12 additions & 0 deletions src/ollama_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ async def chat_completion(system_prompt: str, user_input: str, *, model: str | N
response.raise_for_status()
return _extract_chat_content(response.json())

async def chat_completion_with_messages(messages: list[dict], *, model: str | None = None, temperature: float):
payload = {
"model": model or OLLAMA_MODEL,
"messages": messages,
"stream": False,
"options": {"temperature": temperature},
}

async with httpx.AsyncClient(timeout=OLLAMA_TIMEOUT_SECONDS) as client:
response = await client.post(f"{OLLAMA_URL}/api/chat", json=payload)
response.raise_for_status()
return _extract_chat_content(response.json())

async def _embed_with_modern_api(client: httpx.AsyncClient, texts: list[str], model: str) -> np.ndarray:
response = await client.post(
Expand Down
9 changes: 9 additions & 0 deletions src/service/vulnerabilities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,20 @@
verify_indirect_level_secret,
)

from .bola_chatbot_lab import (
LEVELS as BOLA_LEVELS,
evaluate_level as evaluate_bola_level,
verify_level_secret as verify_bola_level_secret,
)

__all__ = [
"LEVELS",
"evaluate_level",
"verify_level_secret",
"INDIRECT_LEVELS",
"evaluate_indirect_level",
"verify_indirect_level_secret",
"BOLA_LEVELS",
"evaluate_bola_level",
"verify_bola_level_secret",
]
Loading
Loading