Skip to content

FIX: Refuse saved targets that would authenticate with a key (part 7) - #4025

Open
varunj-msft wants to merge 201 commits into
microsoft:mainfrom
varunj-msft:varunj-msft/13178-Refuse-Saved-Target-Keys
Open

varunj-msft wants to merge 201 commits into
microsoft:mainfrom
varunj-msft:varunj-msft/13178-Refuse-Saved-Target-Keys

Conversation

@varunj-msft

@varunj-msft varunj-msft commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Description

Part 7 of saving Registry entries across restarts. Stacked on part 6 (#3986); part 8 (#4026) builds on it. This PR's own changes: varunj-msft/PyRIT@varunj-msft/13178-Launch-Keeps-Saved-Entries...varunj-msft/13178-Refuse-Saved-Target-Keys

Design: 4.4 Credentials. Where every part fits: PR map.

This is one of the changes for saving Registry entries across restarts. It builds on #3986. Until the earlier PRs merge, the diff here also shows their changes.

Problem. Restore refuses a recipe that stores a credential, but a target can use one without storing it. An OpenAI target with an empty api_key reads the key from this server's environment, an empty endpoint takes this server's endpoint setting (which can hold a user name and password), and a URL can carry a SAS token or a key in its query. Saved targets are shared by everyone using the app, so one restored like that would send every user's requests with this server's keys.

Change. Restore now runs every saved entry through one shared check, check_saved_entry_params. On top of the rules it already had (no stored credential, no class that needs the server's upload folder), it refuses a target recipe that:

  • uses an auth mode its class doesn't support, or leaves empty a parameter the class would fill from this server. Each class lists those in server_fallback_parameters, with the value that turns the fallback off (for example, OpenAI's headers set to "{}"). Microsoft Entra ID ("identity") and no key ("none") are fine. Entra ID uses this server's Azure identity the same way targets created through the API do today, and an OpenAI target only mints that token for a recognized Azure endpoint.
  • leaves a destination to this server's settings, or gives one that can carry a credential. Each class lists its destinations in destination_parameters. One that would take this server's setting must be a full http(s) or ws(s) address. None may hold a user name, password, SAS token, fragment, ;, & or = in the path, or a query other than api-version, even after percent-decoding and Unicode normalization, and a path can't have a query at all.
  • builds a class that can't be saved at all, each with its reason in not_saveable_reason: WebSocketCopilotTarget, HTTPTarget and HuggingFaceChatTarget. (HTTPXAPITarget was already refused, since it needs the server's upload folder.)

Each refusal is reported as a restore failure whose message names the class or parameter, never a value.

What stays the same. Converters and scorers get the same checks as before, since they don't declare fallbacks or destinations yet. Targets created without saving, and targets the startup scripts register, aren't checked, so a saved round-robin can still use startup targets, which keep their own sign-in. server_credential_parameter (the admin check from #3247) is unchanged.

Not covered. Environment variables the OpenAI SDK reads by itself (OPENAI_CUSTOM_HEADERS, OPENAI_ORG_ID, OPENAI_PROJECT_ID) still apply to every OpenAI target on this server, saved ones included.

Tests and Documentation

  • New tests/unit/registry/test_registry_recipe_rules.py: Entra ID and no-key targets pass; an OpenAI target without a key or identity, an empty key the class reads from this server, identity without headers, headers that can carry a credential, an auth mode the class doesn't support, and a class that can't be saved are refused; plain addresses and A2A agent card paths pass; URLs and paths that can carry a credential (user info, SAS, fragment, path parameters, other query keys, encoded or look-alike forms, and values that never finish decoding) are refused; an empty destination that would take this server's setting is refused, and one that isn't a full address gets its own message; text parameters that aren't destinations are left alone. Each URL rule also has a case only it catches, such as ; or & alone in a path, sig= or SIG= in the host, a lone api-key, subscription-key or sv query, an api-version value that isn't version-shaped, and an agent endpoint with another scheme, no host, or a value that can't be parsed.
  • tests/unit/registry/test_registry_recipe_restore.py: restore reports a target that would use this server's credentials, or sign in with this server's account, instead of building it. The test that fed a server endpoint holding a credential into a build error is gone, since such a target is now refused before it's built; the tests that build errors hide credentials stay.
  • Each of the 39 ways I tried to remove or loosen a rule fails at least one of these tests.
  • uv run --frozen --extra all python -m pytest -n 8 --dist=loadfile tests/unit/registry tests/unit/backend tests/unit/prompt_target tests/unit/models tests/unit/setup tests/unit/converter tests/unit/score: 19,447 passed, 9 skipped, 4 xfailed
  • pre-commit (ruff, ty) on the changed files: passed.
  • doc/getting_started/pyrit_conf.md: what a saved target can hold and use, which addresses must be full, the OpenAI SDK settings that still apply, and which target types can't be saved yet.

No JupyText changes.

…API input

External construction now rejects a parameter whose type includes Path unless the
calling service passes the name in allowed_path_params after making the value safe.
The converter service allows its upload parameters once they are stored, and the
target service allows the server-set upload folder and the GitHubCopilotTarget
working directory. The scorer-only check is removed, and the "Valid parameters"
list leaves out path parameters callers cannot set.
The target catalog now applies the path rule to required parameters too, so a
target that needs a disallowed path is left out instead of listed without the
field, matching the scorer catalog. allowed_path_params is typed frozenset[str]
so a bare string cannot turn the allow-list into a substring match.
The target catalog still lists a target whose required path parameter is its
server-set upload folder when the server has one, and leaves it out otherwise.
# Conflicts:
#	tests/unit/backend/test_converter_service.py
# Conflicts:
#	pyrit/prompt_target/azure_blob_storage_target.py
The runtime status keeps its short summary. The restore log now also lists each field's full location
and message, escaped, without the stored values, and with URL credentials removed. Credentials are
removed before a location is shortened, so the summary can't keep part of one either.
Its text can carry a SAS URL. The 503 handler no longer logs it as a debug traceback, and the scenario
suite service no longer attaches it to its warning; both log the storage message, which has no credentials.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants