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
32 changes: 24 additions & 8 deletions app.py
Original file line number Diff line number Diff line change
@@ -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 = """
<h1>jwt</h1>
<pre>{token}</pre>
Expand Down Expand Up @@ -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)),
)


Expand All @@ -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)
Expand All @@ -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)
11 changes: 3 additions & 8 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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