fix(detectors): guard against non-string output.text in AttackRogueString#1921
fix(detectors): guard against non-string output.text in AttackRogueString#1921lavkeshdwivedi wants to merge 3 commits into
Conversation
…ring
When a REST generator returns a structured JSON response, output.text can
be a dict rather than a str, causing:
AttributeError: 'dict' object has no attribute 'lower'
at the trigger.lower() / output_text.lower() call in detect().
Add an isinstance guard that coerces non-str output_text to str before
the case-folding and substring check, matching the defensive pattern used
elsewhere in the codebase.
Fixes NVIDIA#1888.
Signed-off-by: Lavkesh Dwivedi <9712103+lavkeshdwivedi@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
f2f4cdf to
3a40833
Compare
jmartin-tech
left a comment
There was a problem hiding this comment.
The signature for detect accepts Attempt objects and output here should be a Message object on that Attempt which should have a text attribute that is defined being str or None.
Defensive guards are used to validate typing, however I am not sure this is the appropriate solution here. The issue reported used the rest generator which may be the more appropriate location to coerce the value to str.
lavkeshdwivedi
left a comment
There was a problem hiding this comment.
Thank you for the direction — you're right. Moved the fix to RestGenerator._get_response() where Message objects are constructed. The coercion now happens at line 406: Message(str(r) if r is not None and not isinstance(r, str) else r), which upholds the Message.text: str | None contract before any detector sees the value. Reverted the isinstance guard from the detector entirely.
…tGenerator Per maintainer review: the correct fix for dict-valued output.text is in the REST generator, not the detector. Message.text is contractually str|None; RestGenerator violated this when response_json_field resolved to a non-string JSON value (e.g. a nested object). Coerce at the point of Message construction so all downstream consumers (detectors, scorers) always receive str|None from Message.text. Reverts the isinstance guard added to detectors/promptinject.py — now unnecessary since the generator upholds the Message.text contract. Fixes NVIDIA#1888. Signed-off-by: Lavkesh Dwivedi <9712103+lavkeshdwivedi@users.noreply.github.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
e9e1860 to
3ef3280
Compare
|
The remaining Windows CI failure (
Willing to add |
|
Tests have been re-triggered, the failure was env related and does not warrant change being to mark as flaky as this is one of the first times it has failed due to a networking issue outside the control of the test itself. |
|
Thanks for the review @jmartin-tech. The current diff already implements the approach you suggested, coercing non-string responses to return [Message(str(r) if r is not None and not isinstance(r, str) else r) for r in response]The |
|
All CI now green including the previously-flaky |
|
Hi @jmartin-tech, I've addressed your feedback. Could you re-review when you get a chance? |
|
Added two regression tests in
Both tests call |
…oercion Adds two tests to validate that _get_response() coerces non-string values (int, float, bool) to str before wrapping in Message objects: - test_json_rest_non_string_response_coerced_to_str: parametrized scalar cases - test_json_rest_non_string_list_coerced_to_str: multi-value list from JSONPath Signed-off-by: Lavkesh Dwivedi <iamlavkesh@gmail.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
d625bc7 to
d201b80
Compare
|
All CI is green across all platforms (Linux, macOS, Windows, arm64) and Python versions (3.10–3.13), including DCO. The two regression tests requested are in place. Could you re-review when you get a chance? |
Summary
When a REST generator returns a structured JSON response,
output.textcan be adictrather than astr, causing a crash inpromptinject.AttackRogueString.detect():Root cause
REST generators that return structured JSON populate
output.textwith the parsed dict. The subsequent.lower()call in the detector then crashes.Fix
Coerce non-string responses to
stringenerators/rest.pybefore wrapping them inMessageobjects. This is the correct layer since the type contract should be enforced at the generator boundary:This ensures
Message.textis alwaysstr | Noneas the type annotation promises, without changing detector logic.Test plan
promptinject.HijackKillHumansagainst a REST endpoint that returns a JSON dict; verify noAttributeErroris raisedFixes #1888.