|
33 | 33 | from google.adk.agents.run_config import RunConfig |
34 | 34 | from google.adk.apps.app import App |
35 | 35 | from google.adk.artifacts.base_artifact_service import ArtifactVersion |
| 36 | +from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService |
36 | 37 | from google.adk.cli import fast_api as fast_api_module |
37 | 38 | from google.adk.cli.fast_api import get_fast_api_app |
38 | 39 | from google.adk.errors.input_validation_error import InputValidationError |
@@ -1701,6 +1702,79 @@ def test_save_artifact(test_app, create_test_session, mock_artifact_service): |
1701 | 1702 | assert stored["artifact"].text == "hello world" |
1702 | 1703 |
|
1703 | 1704 |
|
| 1705 | +def test_save_artifact_rejects_cross_scope_artifact_reference_http_flow( |
| 1706 | + mock_memory_service, |
| 1707 | + mock_agent_loader, |
| 1708 | + mock_eval_sets_manager, |
| 1709 | + mock_eval_set_results_manager, |
| 1710 | +): |
| 1711 | + """The HTTP artifact flow rejects forged cross-scope artifact references.""" |
| 1712 | + session_service = InMemorySessionService() |
| 1713 | + artifact_service = InMemoryArtifactService() |
| 1714 | + test_client = _create_test_client( |
| 1715 | + session_service, |
| 1716 | + artifact_service, |
| 1717 | + mock_memory_service, |
| 1718 | + mock_agent_loader, |
| 1719 | + mock_eval_sets_manager, |
| 1720 | + mock_eval_set_results_manager, |
| 1721 | + ) |
| 1722 | + |
| 1723 | + asyncio.run( |
| 1724 | + session_service.create_session( |
| 1725 | + app_name="victim-app", |
| 1726 | + user_id="victim", |
| 1727 | + session_id="victim-session", |
| 1728 | + state={}, |
| 1729 | + ) |
| 1730 | + ) |
| 1731 | + asyncio.run( |
| 1732 | + session_service.create_session( |
| 1733 | + app_name="attacker-app", |
| 1734 | + user_id="attacker", |
| 1735 | + session_id="attacker-session", |
| 1736 | + state={}, |
| 1737 | + ) |
| 1738 | + ) |
| 1739 | + asyncio.run( |
| 1740 | + artifact_service.save_artifact( |
| 1741 | + app_name="victim-app", |
| 1742 | + user_id="victim", |
| 1743 | + session_id="victim-session", |
| 1744 | + filename="private.txt", |
| 1745 | + artifact=types.Part(text="CROSS_SCOPE_SECRET"), |
| 1746 | + ) |
| 1747 | + ) |
| 1748 | + |
| 1749 | + save_response = test_client.post( |
| 1750 | + "/apps/attacker-app/users/attacker/sessions/attacker-session/artifacts", |
| 1751 | + json={ |
| 1752 | + "filename": "loot.txt", |
| 1753 | + "artifact": { |
| 1754 | + "fileData": { |
| 1755 | + "fileUri": ( |
| 1756 | + "artifact://apps/victim-app/users/victim/sessions/" |
| 1757 | + "victim-session/artifacts/private.txt/versions/0" |
| 1758 | + ), |
| 1759 | + "mimeType": "text/plain", |
| 1760 | + } |
| 1761 | + }, |
| 1762 | + }, |
| 1763 | + ) |
| 1764 | + |
| 1765 | + assert save_response.status_code == 400 |
| 1766 | + assert ( |
| 1767 | + save_response.json()["detail"] |
| 1768 | + == "Artifact references must stay within the same app and user scope." |
| 1769 | + ) |
| 1770 | + |
| 1771 | + load_response = test_client.get( |
| 1772 | + "/apps/attacker-app/users/attacker/sessions/attacker-session/" |
| 1773 | + "artifacts/loot.txt" |
| 1774 | + ) |
| 1775 | + assert load_response.status_code == 404 |
| 1776 | + |
| 1777 | + |
1704 | 1778 | def test_artifact_endpoints_support_nested_names( |
1705 | 1779 | test_app, create_test_session, mock_artifact_service |
1706 | 1780 | ): |
|
0 commit comments