-
Notifications
You must be signed in to change notification settings - Fork 0
Add user registration and login endpoints #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| """ | ||
| Statement export endpoint — converts account statements to PDF/CSV via pandoc. | ||
| """ | ||
| import subprocess | ||
| from flask import Flask, request, send_file | ||
|
|
||
| app = Flask(__name__) | ||
|
|
||
|
|
||
| def _build_export_command(account_id: str, fmt: str) -> tuple[str, str]: | ||
| output_path = f"/tmp/{account_id}.{fmt}" | ||
| cmd = f"pandoc --quiet statements/{account_id}.md -t {fmt} -o {output_path}" | ||
| return cmd, output_path | ||
|
|
||
|
|
||
| def run_export(account_id: str, fmt: str) -> str: | ||
| cmd, output_path = _build_export_command(account_id, fmt) | ||
| subprocess.run(cmd, shell=True, check=False) | ||
| return output_path | ||
|
|
||
|
|
||
| @app.route("/api/accounts/<account_id>/export") | ||
| def export_statement(account_id: str): | ||
| fmt = request.args.get("fmt", "pdf") | ||
| output_path = run_export(account_id, fmt) | ||
| return send_file(output_path) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| """ | ||
| In-app notification renderer — displays alert banners on the dashboard. | ||
| """ | ||
| from flask import Flask, request, render_template_string | ||
|
|
||
| app = Flask(__name__) | ||
|
|
||
| TEMPLATE = """ | ||
| <!DOCTYPE html> | ||
| <html> | ||
| <head><title>Notifications</title></head> | ||
| <body> | ||
| <div id="banner">{{ message }}</div> | ||
| </body> | ||
| </html> | ||
| """ | ||
|
|
||
|
|
||
| @app.route("/notifications") | ||
| def notifications(): | ||
| # Display a one-time alert passed via query string (e.g. from email links) | ||
| message = request.args.get("msg", "") | ||
| return render_template_string(TEMPLATE, message=message) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| """ | ||
| OAuth2 callback handler — exchanges the code for a token and logs the user in. | ||
| """ | ||
| from flask import Flask, request, redirect, session | ||
| import requests | ||
|
|
||
| app = Flask(__name__) | ||
| app.secret_key = "change-me" | ||
|
|
||
| ALLOWED_REDIRECT_DOMAIN = "app.hacktron.ai" | ||
| TOKEN_URL = "https://accounts.google.com/o/oauth2/token" | ||
| USERINFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo" | ||
|
|
||
|
|
||
| def is_valid_redirect(url: str) -> bool: | ||
| from urllib.parse import urlparse | ||
| host = urlparse(url).netloc | ||
| # Intended to allow subdomains of ALLOWED_REDIRECT_DOMAIN | ||
| return host.endswith(ALLOWED_REDIRECT_DOMAIN) | ||
|
Comment on lines
+15
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Steps to ReproduceFix with AITriage: Reply
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. !fixed |
||
|
|
||
|
|
||
| @app.route("/auth/callback") | ||
| def oauth_callback(): | ||
| code = request.args.get("code") | ||
| next_url = request.args.get("next", "/dashboard") | ||
|
|
||
| token_resp = requests.post(TOKEN_URL, data={ | ||
| "code": code, | ||
| "grant_type": "authorization_code", | ||
| }) | ||
| access_token = token_resp.json().get("access_token") | ||
|
|
||
| userinfo = requests.get( | ||
| USERINFO_URL, | ||
| headers={"Authorization": f"Bearer {access_token}"}, | ||
| ).json() | ||
|
|
||
| # email_verified is optional in the OIDC spec — absent means unverified | ||
| if userinfo.get("email_verified") == False: | ||
| return "Email not verified", 403 | ||
|
|
||
| session["user"] = userinfo.get("email") | ||
|
|
||
| if is_valid_redirect(next_url): | ||
| return redirect(next_url) | ||
| return redirect("/dashboard") | ||
|
Comment on lines
+23
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In According to the OpenID Connect (OIDC) specification, the Steps to ReproduceFix with AITriage: Reply
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. !fixed |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| """ | ||
| Webhook proxy — fetches a user-supplied callback URL to deliver transaction events. | ||
| """ | ||
| import requests | ||
| from flask import Flask, request, jsonify | ||
|
|
||
| app = Flask(__name__) | ||
|
|
||
|
|
||
| @app.route("/api/webhooks/deliver", methods=["POST"]) | ||
| def deliver_webhook(): | ||
| body = request.get_json() | ||
| callback_url = body.get("url") | ||
| payload = body.get("payload", {}) | ||
|
|
||
| # Deliver the event to the caller's endpoint | ||
| resp = requests.post(callback_url, json=payload, timeout=5) | ||
| return jsonify({"status": resp.status_code}) | ||
|
Comment on lines
+11
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The While the finding originally stated this could be used to retrieve sensitive metadata, the endpoint only returns the HTTP status code of the response, making this a semi-blind SSRF. Furthermore, the request is hardcoded to use the POST method. However, an attacker can still exploit this to perform internal port scanning, discover internal services by observing status codes, or trigger state-changing operations on internal endpoints that accept POST requests. Steps to Reproducecurl -X POST http://localhost:5000/api/webhooks/deliver -H "Content-Type: application/json" -d '{"url": "http://127.0.0.1:8080/internal-api", "payload": {}}'Fix with AITriage: Reply
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. !fixed |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| """ | ||
| Report download — serves per-user PDF reports from a shared directory. | ||
| """ | ||
| import os | ||
| from flask import Flask, request, send_file, abort | ||
|
|
||
| app = Flask(__name__) | ||
|
|
||
| REPORTS_DIR = "/var/app/reports" | ||
|
|
||
|
|
||
| @app.route("/api/reports/<filename>") | ||
| def download_report(filename: str): | ||
| # Build the path from the caller-supplied filename | ||
| report_path = os.path.join(REPORTS_DIR, filename) | ||
|
|
||
| if not os.path.exists(report_path): | ||
| abort(404) | ||
|
|
||
| return send_file(report_path) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import hashlib | ||
| import uuid | ||
| from flask import request, jsonify | ||
| from functools import wraps | ||
|
|
||
| _users = {} | ||
|
|
||
|
|
||
| def token_required(f): | ||
| @wraps(f) | ||
| def decorated(*args, **kwargs): | ||
| token = request.headers.get("Authorization", "").replace("Bearer ", "") | ||
| if not token: | ||
| return jsonify({"error": "Token missing"}), 401 | ||
| return f({"user_id": 1}, *args, **kwargs) | ||
| return decorated | ||
|
|
||
|
|
||
| def _hash_password(password: str, salt: str) -> str: | ||
| return hashlib.sha256(f"{salt}{password}".encode()).hexdigest() | ||
|
|
||
|
|
||
| def register_routes(app): | ||
| @app.route("/api/users/register", methods=["POST"]) | ||
| def register(): | ||
| data = request.get_json(force=True) | ||
| email = data.get("email", "").strip().lower() | ||
| password = data.get("password", "") | ||
| if not email or not password: | ||
| return jsonify({"error": "email and password required"}), 400 | ||
| if email in _users: | ||
| return jsonify({"error": "email already registered"}), 409 | ||
| salt = uuid.uuid4().hex | ||
| _users[email] = {"salt": salt, "password_hash": _hash_password(password, salt)} | ||
| return jsonify({"message": "registered"}), 201 | ||
|
|
||
| @app.route("/api/users/login", methods=["POST"]) | ||
| def login(): | ||
| data = request.get_json(force=True) | ||
| email = data.get("email", "").strip().lower() | ||
| password = data.get("password", "") | ||
| user = _users.get(email) | ||
| if not user or _hash_password(password, user["salt"]) != user["password_hash"]: | ||
| return jsonify({"error": "invalid credentials"}), 401 | ||
| return jsonify({"token": "session-token-placeholder"}) | ||
|
Comment on lines
+38
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Tracegraph TD
subgraph SG0 ["users.py"]
hash_password["Hashes a password with a salt using SHA-256."]
login{{"Handles user login by verifying credentials against stored password hashes."}}
end
style SG0 fill:#2a2a2a,stroke:#444,color:#aaa
login --> hash_password
Fix with AITriage: Reply
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. !fixed in commit XYZ
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. !fixed in commit GGG |
||
|
|
||
| @app.route("/api/users/me", methods=["GET"]) | ||
| @token_required | ||
| def me(current_user): | ||
| return jsonify(current_user) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
run_exportfunction inweb789/export.pyconstructs a command string usingaccount_idandfmtand executes it usingsubprocess.run(cmd, shell=True). Sinceaccount_idandfmtare taken directly from the request without sanitization, an attacker can inject arbitrary shell commands. For example, an attacker can provide anaccount_idliketest; cat /etc/passwd #to execute unintended commands.Steps to Reproduce
Fix with AI
Triage: Reply
!fp <reason>(false positive),!valid(confirmed), or!accepted_risk <reason>. Any other reply is saved as a triage note.Reason is optional but improves future scans — e.g.
!fp internal endpoint, not user-facing.View finding in Hacktron
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alan Lim commented: