diff --git a/app.py b/app.py index 715145c..90a9daa 100644 --- a/app.py +++ b/app.py @@ -1,12 +1,17 @@ +import hmac import os import pprint from flask import Flask, request -from jose import ExpiredSignatureError, JWTError, jwt +from jose import JWTError, jwt +from markupsafe import escape app = Flask(__name__, static_url_path="/", static_folder="public") SECRET_HEADER = "X-Perc-App-Secret" +# Only symmetric HMAC is expected; pin it so the token's own header can't +# select a different (or weaker) algorithm. +ALLOWED_ALGORITHMS = ["HS256"] SHOW_TOKEN_HTML = """
{token}
@@ -40,12 +45,20 @@ def echo_jwt():
return "No JWT found in request"
try:
- payload = jwt.decode(token, app_secret, options={"verify_aud": False})
+ payload = jwt.decode(
+ token,
+ app_secret,
+ algorithms=ALLOWED_ALGORITHMS,
+ options={"verify_aud": False},
+ )
except JWTError:
- return SHOW_INVALID_TOKEN_HTML.format(token=token)
+ # token is attacker-controlled and reflected into the response, so it
+ # MUST be HTML-escaped to prevent reflected XSS.
+ return SHOW_INVALID_TOKEN_HTML.format(token=escape(token))
return SHOW_TOKEN_HTML.format(
- token=token, decoded_token=pprint.pformat(payload, indent=4)
+ token=escape(token),
+ decoded_token=escape(pprint.pformat(payload, indent=4)),
)
@@ -60,15 +73,15 @@ def lifecycle_callback():
app_secret = get_app_secret()
if not app_secret:
print("Cannot validate request, no APP_SECRET environment variable defined")
- elif header_secret != app_secret:
+ elif not header_secret or not hmac.compare_digest(header_secret, app_secret):
print(
"WARNING! This request may not have come from Percolate, the "
"X-Perc-App-Secret header value does not match the APP_SECRET environment "
"variable!"
)
- message = '"/{}" lifecycle callback endpoint called with data: \n{}'.format(
- request.base_url.split("/")[-1], request.get_json()
+ message = '"{}" lifecycle callback endpoint called with data: \n{}'.format(
+ request.path, request.get_json()
)
print(message)
return "{}\n".format(message)
@@ -80,4 +93,7 @@ def hello():
if __name__ == "__main__":
- app.run(debug=True, port=8000)
+ # Never default to the Werkzeug debugger in a network-exposed app (the
+ # README walks users through exposing this via ngrok). Opt in explicitly.
+ debug = os.environ.get("FLASK_DEBUG", "").lower() in ("1", "true", "yes")
+ app.run(debug=debug, port=8000)
diff --git a/requirements.txt b/requirements.txt
index 9f9c1d3..8a3f6d2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1,3 @@
-Click==7.0
-Flask==1.1.1
-gunicorn==19.9.0
-itsdangerous==1.1.0
-Jinja2==2.10.1
-MarkupSafe==1.1.1
-Werkzeug==0.15.5
-python-jose==3.0.1
+Flask>=3.0.3
+gunicorn>=22.0.0
+python-jose>=3.4.0