-
Notifications
You must be signed in to change notification settings - Fork 87
Differentiate authentication/request error messages (#274) #3633
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: master
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 |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import functools | ||
| import math | ||
| from flask import Flask, jsonify, request, abort | ||
| from sqlalchemy import and_, func, or_ | ||
| from werkzeug.exceptions import HTTPException | ||
|
|
@@ -11,8 +12,14 @@ | |
|
|
||
| @app.before_request | ||
| def verify_secret(): | ||
| if not app.debug and request.headers.get("x-aws-secret") != app.config["AWS_SECRET"]: | ||
| abort(401) | ||
| if app.debug: | ||
| return | ||
|
|
||
| secret = request.headers.get("x-aws-secret") | ||
| if secret is None: | ||
| abort(401, "Missing 'x-aws-secret' header.") | ||
| if secret != app.config["AWS_SECRET"]: | ||
| abort(401, "Invalid 'x-aws-secret' header value.") | ||
|
|
||
|
|
||
| @app.errorhandler(HTTPException) | ||
|
|
@@ -22,11 +29,36 @@ def jsonify_http_error(error): | |
| return jsonify(response), error.code | ||
|
|
||
|
|
||
| MAX_PARAM_ECHO_LEN = 50 | ||
|
|
||
|
|
||
| def _truncate_param(value): | ||
| value = str(value) | ||
| if len(value) > MAX_PARAM_ECHO_LEN: | ||
| return value[:MAX_PARAM_ECHO_LEN] + "..." | ||
| return value | ||
|
|
||
|
|
||
| def paginate_results(f): | ||
| @functools.wraps(f) | ||
| def decorated_function(*args, **kwargs): | ||
| limit = int(request.args.get("limit", 50)) | ||
| page = int(request.args.get("page", 1)) | ||
| limit_param = request.args.get("limit", 50) | ||
| page_param = request.args.get("page", 1) | ||
|
|
||
| try: | ||
| limit = int(limit_param) | ||
| except (TypeError, ValueError): | ||
| abort(400, f"Invalid 'limit' query parameter: '{_truncate_param(limit_param)}' is not an integer.") | ||
|
|
||
| try: | ||
| page = int(page_param) | ||
| except (TypeError, ValueError): | ||
| abort(400, f"Invalid 'page' query parameter: '{_truncate_param(page_param)}' is not an integer.") | ||
|
|
||
| if limit < 1: | ||
| abort(400, "Invalid 'limit' query parameter: must be >= 1.") | ||
| if page < 1: | ||
| abort(400, "Invalid 'page' query parameter: must be >= 1.") | ||
|
|
||
| queryset = f(*args, **kwargs).paginate(page=page, per_page=limit, max_per_page=100) | ||
|
Contributor
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. Main finding: range is still unvalidated, so the misleading-error case from #274 survives on this exact code path. The
So after this PR:
Those are exactly the "error message does not tell you what is actually wrong" reports #274 is about, and arguably more common in practice than passing a non-integer. A caller who typos Suggested minimum: after the two if limit < 1:
abort(400, f"Invalid 'limit' query parameter: must be >= 1 (max 100).")
if page < 1:
abort(400, f"Invalid 'page' query parameter: must be >= 1.")and consider whether an out-of-range page should stay a 404 or become a 400 with a message naming the last valid page ( Two related edge cases while you are here:
Note also that |
||
| result = { | ||
|
|
@@ -123,7 +155,14 @@ def api_hadiths(): | |
|
|
||
| chapter_id = request.args.get("chapterId") | ||
| if chapter_id: | ||
| query = query.filter_by(babID=float(chapter_id)) | ||
| try: | ||
| parsed_chapter_id = float(chapter_id) | ||
| except (TypeError, ValueError): | ||
| abort(400, f"Invalid 'chapterId' query parameter: '{_truncate_param(chapter_id)}' is not a number.") | ||
| if not math.isfinite(parsed_chapter_id): | ||
| abort(400, f"Invalid 'chapterId' query parameter: '{_truncate_param(chapter_id)}' is not a finite number.") | ||
| chapter_id = parsed_chapter_id | ||
| query = query.filter_by(babID=chapter_id) | ||
|
|
||
| hadith_number = request.args.get("hadithNumber") | ||
| if hadith_number: | ||
|
|
||
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.
Low / informational. Two small things about differentiating these two 401s:
The response body now names the internal auth header (
x-aws-secret) to unauthenticated callers, which the previous generic Werkzeug message did not. That is a small disclosure of the internal gateway mechanism. If the intent is to help legitimate integrators, naming the header in the "missing" case is defensible; echoing it again in the "invalid value" case adds nothing they do not already know and gives a scanner a positive signal that the header is the right one. Consider collapsing the second message to something that does not confirm header correctness, or keep both and accept the (small) tradeoff deliberately.Unrelated to this PR but adjacent to the line you are editing:
secret != app.config["AWS_SECRET"]is a non-constant-time comparison. Pre-existing, not introduced here — flagging only because you are already in this block;hmac.compare_digestwould be a one-line hardening if you want it, otherwise leave it for a separate change.