Skip to content
Open
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
7 changes: 6 additions & 1 deletion blt/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"dj_rest_auth.registration",
"storages",
"channels",

)

if DEBUG:
Expand Down Expand Up @@ -165,7 +166,7 @@
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"DIRS": ['templates'],
"APP_DIRS": False,
"OPTIONS": {
"debug": DEBUG,
Expand Down Expand Up @@ -629,3 +630,7 @@
}
THROTTLE_WINDOW = 60 # 60 seconds (1 minute)
THROTTLE_EXEMPT_PATHS = ["/admin/", "/static/", "/media/"]

from dotenv import load_dotenv
load_dotenv()
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
Comment on lines +634 to +636
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consolidate environment variable loading and add API key validation.

There are multiple approaches to loading environment variables in this file. The existing code uses environ.Env.read_env() at lines 14 and 19, and now load_dotenv() is added. This creates redundancy and potential confusion.

Apply this diff to consolidate and add validation:

-from dotenv import load_dotenv
-load_dotenv()
-GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
+GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
+if not GEMINI_API_KEY:
+    raise Exception("GEMINI_API_KEY environment variable is required for Gemini integration.")

The existing environ.Env.read_env() calls already handle loading from .env files, making load_dotenv() redundant.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from dotenv import load_dotenv
load_dotenv()
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
if not GEMINI_API_KEY:
raise Exception("GEMINI_API_KEY environment variable is required for Gemini integration.")
🤖 Prompt for AI Agents
In blt/settings.py around lines 634 to 636, remove the redundant load_dotenv()
call and the import from dotenv since environ.Env.read_env() is already used
earlier to load environment variables. Then, add validation to ensure
GEMINI_API_KEY is set by checking if the environment variable is None or empty
and raise an appropriate error if it is missing. This consolidates environment
loading and ensures the API key is validated.

10 changes: 10 additions & 0 deletions blt/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,14 @@

handler404 = "website.views.core.handler404"
handler500 = "website.views.core.handler500"
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
path('', include('website.urls')),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify URL routing doesn't conflict with existing patterns.

Including website.urls at the root path could potentially conflict with existing URL patterns defined later in the file.


🏁 Script executed:

#!/bin/bash
# Description: Check for potential URL conflicts between website.urls and existing patterns
# Expected: Find any URL patterns that might conflict

# Check what URLs are defined in website.urls
echo "=== URLs in website/urls.py ==="
cat website/urls.py

echo -e "\n=== Checking for potential conflicts ==="
# Look for patterns that might conflict with home, api/test-gemini/, ask-gemini/
rg -A 2 -B 2 "(path\(''\)|path\('api/\)|path\('ask-)" blt/urls.py

Length of output: 605


🏁 Script executed:

#!/bin/bash
set -e

echo "URL prefixes in blt/urls.py:"
# single-quoted
rg -o "path\('[^']*'" blt/urls.py | sed -E "s/path\('([^']*)'/\1/"
# double-quoted
rg -o 'path\("[^"]*"' blt/urls.py | sed -E 's/path\("([^"]*)"/\1/'

echo -e "\nURL prefixes in website/urls.py:"
rg -o "path\('[^']*'" website/urls.py | sed -E "s/path\('([^']*)'/\1/"
rg -o 'path\("[^"]*"' website/urls.py | sed -E 's/path\("([^"]*)"/\1/'

Length of output: 4625


Ensure website.urls doesn’t override all other routes

Including website.urls at the empty prefix as the first entry in blt/urls.py (line 367) will capture every request and, since website/urls.py only defines:

  • '' (home)
  • api/test-gemini/
  • ask-gemini/

any other URL in your project will 404 instead of matching its intended pattern.

Please adjust one of the following:

  • Move
    blt/urls.py:367
  • path('', include('website.urls')),
  • …other patterns here…

  • path('', include('website.urls')), # move below existing routes
  • Or mount under its own prefix, e.g.:
  • path('', include('website.urls')),
  • path('website/', include('website.urls')),

This will ensure your core app and API routes remain reachable.

🤖 Prompt for AI Agents
In blt/urls.py at line 367, including 'website.urls' at the root path causes it
to override all other routes, leading to 404 errors for URLs not defined there.
To fix this, either move this include statement below other route definitions so
those routes take precedence, or mount 'website.urls' under a specific prefix
(e.g., 'website/') to isolate its URL space and prevent it from capturing all
requests.

path(settings.ADMIN_URL + "/", admin.site.urls),
Comment on lines +361 to +368
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove redundant imports and duplicate admin URL.

The imports at lines 361-364 are redundant as they're already imported at the top of the file. Additionally, there's now a duplicate admin URL pattern.

Apply this diff to clean up the redundancy:

-from django.contrib import admin
-from django.urls import path, include
-from django.conf import settings
-from django.conf.urls.static import static
-
 urlpatterns = [
     path('', include('website.urls')),
-    path(settings.ADMIN_URL + "/", admin.site.urls),

The admin URL is already defined at line 528, making the duplicate at line 368 unnecessary.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('', include('website.urls')),
path(settings.ADMIN_URL + "/", admin.site.urls),
urlpatterns = [
path('', include('website.urls')),
]
🤖 Prompt for AI Agents
In blt/urls.py around lines 361 to 368, remove the redundant imports of admin,
path, include, settings, and static since they are already imported at the top
of the file. Also, delete the duplicate admin URL pattern at line 368 because
the admin URL is already defined at line 528, avoiding duplication in
urlpatterns.

path("simulation/", dashboard, name="dashboard"),
path("banned_apps/", BannedAppsView.as_view(), name="banned_apps"),
path("api/banned_apps/search/", search_banned_apps, name="search_banned_apps"),
Expand Down Expand Up @@ -1106,3 +1112,7 @@
re_path(r"^__debug__/", include(debug_toolbar.urls)),
] + urlpatterns
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)




49 changes: 49 additions & 0 deletions website/templates/gemini_input.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ask Gemini</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
</head>
<body class="bg-white text-gray-800 font-sans">
<nav class="bg-white shadow-md p-4">
<div class="text-red-600 font-bold text-2xl">OWASP BLT</div>
</nav>

<main class="max-w-3xl mx-auto mt-12 px-4">
<h1 class="text-3xl font-bold text-gray-900 mb-4">Ask Gemini</h1>
<form id="gemini-form" class="mb-6">
<textarea id="user-input" rows="4" class="w-full border rounded-md p-4 focus:outline-none focus:ring-2 focus:ring-red-500" placeholder="Type your question..."></textarea>
<button type="submit" class="mt-4 bg-red-500 text-white px-6 py-2 rounded-md hover:bg-red-600">Submit</button>
</form>

<div id="response-container" class="prose max-w-none"></div>
</main>

<script>
const form = document.getElementById('gemini-form');
const input = document.getElementById('user-input');
const responseContainer = document.getElementById('response-container');

form.addEventListener('submit', async (e) => {
e.preventDefault();
const prompt = input.value.trim();
if (!prompt) return;

responseContainer.innerHTML = '<p class="text-gray-500">Loading...</p>';

const response = await fetch('/api/test-gemini/?prompt=' + encodeURIComponent(prompt));
const result = await response.json();
Comment on lines +37 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use POST (with JSON body & CSRF token) instead of GET to avoid URL length limits and query-string leakage

Long prompts will break on browsers/servers that cap URL size, and sensitive questions end up in logs and browser history. Switch to POST with a JSON payload (and pass Django’s csrftoken).

-      const response = await fetch('/api/test-gemini/?prompt=' + encodeURIComponent(prompt));
-      const result = await response.json();
+      const response = await fetch('/api/test-gemini/', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+          'X-CSRFToken': getCookie('csrftoken')  // helper required
+        },
+        body: JSON.stringify({ prompt })
+      });
+      const result = await response.json();

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In website/templates/gemini_input.html around lines 37 to 38, the code uses a
GET request with query parameters to send the prompt, which risks URL length
limits and leaks sensitive data in logs and history. Change the fetch call to
use POST instead, sending the prompt as a JSON body. Also, include the CSRF
token from Django in the request headers to ensure security. Update the
server-side endpoint to accept POST requests with JSON if needed.


if (result.response) {
const formatted = marked.parse(result.response);
responseContainer.innerHTML = formatted;
} else {
Comment on lines +40 to +43
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Sanitize Gemini-generated HTML before injecting it into the DOM

marked.parse() will happily render any HTML the model returns – including <script> tags – leading to a trivial stored/ reflected XSS vector. Never trust LLM output.

-        const formatted = marked.parse(result.response);
-        responseContainer.innerHTML = formatted;
+        // Sanitize to neutralise any hostile markup returned by the model
+        const formatted = DOMPurify.sanitize(marked.parse(result.response));
+        responseContainer.innerHTML = formatted;

Add DOMPurify (or enable marked’s sanitize option, but DOMPurify is battle-tested):

<!-- add right after the marked.js import -->
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.5/dist/purify.min.js"
        integrity="sha384-8W+bKHSN50dBprVT2z5jc4azlD2r7rSnPv4XLLF9B8g8nMiZxgJ5Bf5+8b5hvx+H"
        crossorigin="anonymous"></script>
🤖 Prompt for AI Agents
In website/templates/gemini_input.html around lines 40 to 43, the code uses
marked.parse() to convert model output to HTML and injects it directly into the
DOM, which can lead to XSS vulnerabilities if the output contains malicious
scripts. To fix this, include the DOMPurify library by adding its script tag
after the marked.js import, then sanitize the HTML returned by marked.parse()
using DOMPurify.sanitize() before assigning it to responseContainer.innerHTML.

responseContainer.innerHTML = '<p class="text-red-600">' + (result.error || 'Something went wrong.') + '</p>';
}
});
</script>
</body>
</html>
9 changes: 9 additions & 0 deletions website/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.urls import path
from website.views.core import home
from website.views.gemini import test_gemini, gemini_ui # <-- use gemini_ui, not gemini_input_page

urlpatterns = [
path('', home, name='home'),
path('api/test-gemini/', test_gemini, name='test_gemini'),
path('ask-gemini/', gemini_ui, name='gemini_ui'),
]
32 changes: 32 additions & 0 deletions website/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,38 @@
from openai import OpenAI
from PIL import Image

# website/utils.py
import requests
from django.conf import settings

def call_gemini(prompt: str) -> str:
url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent"
headers = {"Content-Type": "application/json"}

# Add domain-specific context to act like a security assistant
context_prompt = (
"You are Gemini, a helpful, polite assistant integrated into OWASP BLT, a suite of security tools. "
"You help users understand cybersecurity, bug bounty, appsec, secure coding, OWASP Top 10, and other web security concerns. "
"Provide clear, well-formatted answers suitable for developers and researchers. Respond in markdown format."
)

full_prompt = f"{context_prompt}\n\nUser's question: {prompt}"

payload = {
"contents": [{"parts": [{"text": full_prompt}]}]
}

params = {"key": settings.GEMINI_API_KEY}
response = requests.post(url, headers=headers, json=payload, params=params)

data = response.json()
if "candidates" in data:
return data["candidates"][0]["content"]["parts"][0]["text"]
else:
return str(data)

Comment on lines +31 to +56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve security, error handling, and reliability.

Several improvements needed for production readiness:

  1. API key should be in headers, not query parameters
  2. Missing error handling for API response status
  3. No timeout specified for HTTP request
  4. Response parsing could fail if structure is unexpected

Apply this diff to address these issues:

 def call_gemini(prompt: str) -> str:
     url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent"
-    headers = {"Content-Type": "application/json"}
+    headers = {
+        "Content-Type": "application/json",
+        "x-goog-api-key": settings.GEMINI_API_KEY
+    }

     # Add domain-specific context to act like a security assistant
     context_prompt = (
         "You are Gemini, a helpful, polite assistant integrated into OWASP BLT, a suite of security tools. "
         "You help users understand cybersecurity, bug bounty, appsec, secure coding, OWASP Top 10, and other web security concerns. "
         "Provide clear, well-formatted answers suitable for developers and researchers. Respond in markdown format."
     )

     full_prompt = f"{context_prompt}\n\nUser's question: {prompt}"

     payload = {
         "contents": [{"parts": [{"text": full_prompt}]}]
     }

-    params = {"key": settings.GEMINI_API_KEY}
-    response = requests.post(url, headers=headers, json=payload, params=params)
+    response = requests.post(url, headers=headers, json=payload, timeout=30)
+    response.raise_for_status()  # Raise exception for HTTP errors

     data = response.json()
-    if "candidates" in data:
+    if "candidates" in data and len(data["candidates"]) > 0:
+        parts = data["candidates"][0].get("content", {}).get("parts", [])
+        if parts and len(parts) > 0:
+            return parts[0].get("text", "No response generated")
+        else:
+            return "No response content available"
-        return data["candidates"][0]["content"]["parts"][0]["text"]
     else:
-        return str(data)
+        error_msg = data.get("error", {}).get("message", "Unknown API error")
+        raise Exception(f"Gemini API error: {error_msg}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def call_gemini(prompt: str) -> str:
url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent"
headers = {"Content-Type": "application/json"}
# Add domain-specific context to act like a security assistant
context_prompt = (
"You are Gemini, a helpful, polite assistant integrated into OWASP BLT, a suite of security tools. "
"You help users understand cybersecurity, bug bounty, appsec, secure coding, OWASP Top 10, and other web security concerns. "
"Provide clear, well-formatted answers suitable for developers and researchers. Respond in markdown format."
)
full_prompt = f"{context_prompt}\n\nUser's question: {prompt}"
payload = {
"contents": [{"parts": [{"text": full_prompt}]}]
}
params = {"key": settings.GEMINI_API_KEY}
response = requests.post(url, headers=headers, json=payload, params=params)
data = response.json()
if "candidates" in data:
return data["candidates"][0]["content"]["parts"][0]["text"]
else:
return str(data)
def call_gemini(prompt: str) -> str:
url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent"
headers = {
"Content-Type": "application/json",
"x-goog-api-key": settings.GEMINI_API_KEY
}
# Add domain-specific context to act like a security assistant
context_prompt = (
"You are Gemini, a helpful, polite assistant integrated into OWASP BLT, a suite of security tools. "
"You help users understand cybersecurity, bug bounty, appsec, secure coding, OWASP Top 10, and other web security concerns. "
"Provide clear, well-formatted answers suitable for developers and researchers. Respond in markdown format."
)
full_prompt = f"{context_prompt}\n\nUser's question: {prompt}"
payload = {
"contents": [{"parts": [{"text": full_prompt}]}]
}
response = requests.post(url, headers=headers, json=payload, timeout=30)
response.raise_for_status() # Raise exception for HTTP errors
data = response.json()
if "candidates" in data and len(data["candidates"]) > 0:
parts = data["candidates"][0].get("content", {}).get("parts", [])
if parts and len(parts) > 0:
return parts[0].get("text", "No response generated")
else:
return "No response content available"
else:
error_msg = data.get("error", {}).get("message", "Unknown API error")
raise Exception(f"Gemini API error: {error_msg}")
🤖 Prompt for AI Agents
In website/utils.py lines 31 to 56, the call_gemini function has multiple
issues: the API key should be moved from query parameters to the request headers
for better security; add error handling to check the HTTP response status and
raise or handle errors if the call fails; specify a timeout parameter in the
requests.post call to avoid hanging requests; and add robust parsing of the JSON
response to safely handle unexpected or missing fields, using try-except blocks
or conditional checks to prevent runtime errors.



from website.models import DailyStats

from .models import PRAnalysisReport
Expand Down
16 changes: 16 additions & 0 deletions website/views/gemini.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.shortcuts import render
from django.http import JsonResponse
from website.utils import call_gemini

def gemini_ui(request):
return render(request, "gemini_input.html") # <-- match your actual path

def test_gemini(request):
prompt = request.GET.get("prompt")
if not prompt:
return JsonResponse({"error": "No prompt provided"}, status=400)
try:
response = call_gemini(prompt)
return JsonResponse({"response": response})
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.
Comment on lines +8 to +16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix information exposure vulnerability.

The exception handling exposes full error details to users, which could leak sensitive information like API keys, internal paths, or system configuration details.

Apply this diff to implement secure error handling:

 def test_gemini(request):
     prompt = request.GET.get("prompt")
     if not prompt:
         return JsonResponse({"error": "No prompt provided"}, status=400)
     try:
         response = call_gemini(prompt)
         return JsonResponse({"response": response})
     except Exception as e:
-        return JsonResponse({"error": str(e)}, status=500)
+        # Log the full error for debugging but return generic message to user
+        import logging
+        logging.error(f"Gemini API error: {str(e)}")
+        return JsonResponse({"error": "An error occurred while processing your request"}, status=500)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_gemini(request):
prompt = request.GET.get("prompt")
if not prompt:
return JsonResponse({"error": "No prompt provided"}, status=400)
try:
response = call_gemini(prompt)
return JsonResponse({"response": response})
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)
def test_gemini(request):
prompt = request.GET.get("prompt")
if not prompt:
return JsonResponse({"error": "No prompt provided"}, status=400)
try:
response = call_gemini(prompt)
return JsonResponse({"response": response})
except Exception as e:
# Log the full error for debugging but return a generic message to the user
import logging
logging.error(f"Gemini API error: {e}")
return JsonResponse(
{"error": "An error occurred while processing your request"},
status=500
)
🧰 Tools
🪛 GitHub Check: CodeQL

[warning] 16-16: Information exposure through an exception
Stack trace information flows to this location and may be exposed to an external user.

🤖 Prompt for AI Agents
In website/views/gemini.py around lines 8 to 16, the current exception handling
returns the full error message to the user, risking exposure of sensitive
information. Modify the except block to log the detailed exception internally
for debugging but return a generic error message to the user in the
JsonResponse, such as "An internal error occurred," with a 500 status code to
prevent leaking sensitive details.

Loading