fix(security): replace str(e) and user-input echoes in HTTP responses with static messages#5918
fix(security): replace str(e) and user-input echoes in HTTP responses with static messages#5918rtibblesbot wants to merge 8 commits into
Conversation
…patterns Covers all three antipatterns: static message enforcement for str(e) in HttpResponse* bodies (Pattern 1), no echo of user-supplied values in 4xx bodies (Pattern 2), and static strings in change errors lists (Pattern 3). Also includes tests for channel and user viewset error list staticness. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…y response bodies Replaces all Pattern 1 (str(e) in HttpResponse* bodies) and Pattern 2 (user-input formatted into HttpResponse* bodies) antipatterns across all affected handlers in views/internal.py. Static messages replace dynamic content; handle_server_error or logger.warning/exception retains diagnostic detail server-side. Also converts one raise HttpResponseBadRequest to return. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… from views/base, views/users, views/nodes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…and JSON error body with static messages (patterns 1+3) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… user.py error lists with static messages (pattern 3) Fixes Pattern 3 across all remaining viewsets. Also chains ValidationError with from e in validate_completion_criteria, and normalises the _handle_relationship_changes error value to a list for consistency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ent exception text leakage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…changes ValidationError messages like "Channel is not ready to be published" are intentional user-facing validation messages, not internal exception details. Split the except clause so ValidationError propagates e.detail while generic Exception still returns a static "Internal server error". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rtibbles
left a comment
There was a problem hiding this comment.
This is looking mostly right - don't need the Python tests.
| self.assertEqual(response.status_code, 200, response.content) | ||
|
|
||
|
|
||
| class Pattern1StaticErrorBodyTestCase(StudioTestCase): |
There was a problem hiding this comment.
These added tests seem pointless as long as we continue to review CodeQL alerts. please remove them.
There was a problem hiding this comment.
Removed — all five test classes and their imports are gone.
| ) | ||
| raise NotFound(msg) | ||
| except InvalidPage: | ||
| raise NotFound("Invalid page: {}".format(page_number)) |
There was a problem hiding this comment.
We should continue to use the self.invalid_message template here - we just need to change the message to not be the exception. We should also raise from the exc for the NotFound exception.
There was a problem hiding this comment.
Fixed: restored self.invalid_page_message.format(page_number=page_number, message="Invalid page") and chained with raise NotFound(msg) from exc.
There was a problem hiding this comment.
Fixed — kept self.invalid_page_message.format(page_number=page_number, message="Invalid page") and added raise NotFound(msg) from exc.
| except SuspiciousOperation as e: | ||
| return HttpResponseBadRequest(str(e)) | ||
| except SuspiciousOperation: | ||
| logger.warning("SuspiciousOperation in api_file_upload", exc_info=True) |
There was a problem hiding this comment.
I did not know about the exc_info kwarg, handy!
There was a problem hiding this comment.
Glad it's useful — it's the standard way to include the traceback in a warning-level log without using logger.exception (which logs at ERROR).
| except Exception as e: | ||
| log_sync_exception(e, user=self.request.user, change=change) | ||
| change["errors"] = [str(e)] | ||
| change["errors"] = ["Internal server error"] |
There was a problem hiding this comment.
For all of these exceptions, I think this is probably the right call - and actual serialization validation errors would not fall through to this.
There was a problem hiding this comment.
Agreed — DRF ValidationError carries programmer-authored messages (not raw exception text), so propagating its detail is the right call for validation failures vs generic server errors.
| logger.exception("_handle_relationship_changes IntegrityError") | ||
| for change in valid_changes: | ||
| change.update({"errors": str(e)}) | ||
| change.update({"errors": ["Internal server error"]}) |
There was a problem hiding this comment.
Can we be a bit more specific when we have a specific class of error? We know this is because of trying to create something that already exists, usually?
There was a problem hiding this comment.
Changed to "Relationship already exists" — more specific to the IntegrityError cause in this context.
There was a problem hiding this comment.
Changed to "Relationship already exists" — IntegrityErrors in this path are duplicate relationship inserts.
…, improve IntegrityError message - Remove added test classes: reviewer notes CodeQL alert review makes them redundant - pagination.py: restore self.invalid_page_message template usage, replace str(exc) with static "Invalid page", chain with raise...from exc - contentnode.py: use specific "Relationship already exists" message for IntegrityError in _handle_relationship_changes instead of generic "Internal server error" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Fix CodeQL-flagged
py/stack-trace-exposureandpy/reflective-xssalerts acrosscontentcuration/. Exception text and user-input echoes in HTTP response bodies are replaced with static messages; diagnostic detail is retained server-side via existinghandle_server_error()or newlogger.exception()calls.Three patterns swept across 10 files (~25 sites in
views/internal.py, smaller clusters inviewsets/base.py,viewsets/contentnode.py,viewsets/channel.py,viewsets/user.py,views/base.py,views/users.py,views/nodes.py,utils/pagination.py):str(e)inHttpResponse*bodies → static"Internal server error"withcontent_type="text/plain"HttpResponse*bodies → static message withcontent_type="text/plain"str(e)inchange["errors"]lists in viewset mixins →"Internal server error"References
Fixes #5916
Reviewer guidance
Three areas warrant careful review:
views/internal.py— wide sweep: Each of the ~25exceptblocks was independently modified. Spot-check that every site retains eitherhandle_server_error(...)orlogger.warning/exception(...)before the static response.views/internal.py:check_user_is_editor: Araise HttpResponseBadRequest(...)(raising a response object as an exception — a pre-existing bug) was converted toreturn. Verify the caller is not catching an exception here that now won't be raised.viewsets/contentnode.py:move(): Return type changed fromstr(ValidationError(...))to a plain string literal. The caller wraps the return value inchange["errors"] = [move_error]— confirm the list-wrapping still works correctly.AI usage
Implemented with Claude Code working from an autonomous implementation plan. Each commit was scoped to one pattern/file group.
@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?