fix(security): F14 (Astra P1) — KI-Datenrichtlinie deckt JSON-Strings, Provider-Compliance und Tool-Antworten ab
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
Vorher (Astra): (1) enforce_data_policy filterte nur dict-Inhalte — ein JSON-String mit smtp_password passierte ungefiltert (Astra-Repro). (2) agent_runner rief die Policy mit db=None auf — Provider-Compliance (Datenresidenz/erlaubte Datenklassen) wurde NIE geladen. (3) Werkzeugantworten entstehen INNERHALB der ReAct-Schleife — die Policy lief nur davor, Tool-Ergebnisse erreichten den Provider ungefiltert. Fix: - data_policy.py: _filter_json_string_content — JSON-serialisierte Strings werden geparst, durch dieselbe dict-Filterung geleitet und zurueckserialisiert; Nicht-JSON-Strings bleiben unveraendert - agent_runner.py: echte DB-Session (Factory + Tenant-Kontext) statt db=None — Provider-Compliance wird tatsaechlich geladen - agent_loop.py: _filter_observation — jede Tool-Observation wird VOR dem Feed-Back in die LLM-Konversation durch die SENSITIVE_FIELDS-Filterung geleitet (JSON geparst, sensible Felder entfernt, zurueckserialisiert) Abnahme (Astra): Gesperrte Felder fehlen am Provider-Eingang sowohl im Startkontext (durch echte Compliance-Session) als auch nach Werkzeugaufrufen (Observation-Filter) — erfuellt. Verifikation: test_agent_loop + test_phase_f_agents 57 passed/3 skipped (dokumentierte F11-Verweise), Syntax + ruff clean.
This commit is contained in:
@@ -82,6 +82,14 @@ async def enforce_data_policy(
|
||||
else c
|
||||
for c in content
|
||||
]
|
||||
elif isinstance(content, str):
|
||||
# F14 (Astra P1): JSON-serialized strings passed through
|
||||
# UNFILTERED before — a payload like '{"smtp_password": ...}'
|
||||
# reached the provider verbatim. Parse, filter, re-serialize;
|
||||
# non-JSON strings stay unchanged (plain prose is fine).
|
||||
content = _filter_json_string_content(
|
||||
content, metadata, compliance, agent_definition
|
||||
)
|
||||
new_msg = dict(msg)
|
||||
new_msg["content"] = content
|
||||
filtered.append(new_msg)
|
||||
@@ -89,6 +97,44 @@ async def enforce_data_policy(
|
||||
return filtered
|
||||
|
||||
|
||||
def _filter_json_string_content(
|
||||
content: str,
|
||||
metadata: AIUseCaseMetadata,
|
||||
compliance: dict[str, Any] | None,
|
||||
agent_definition: Any,
|
||||
) -> str:
|
||||
"""Filter a JSON-serialized string payload (F14).
|
||||
|
||||
Tries to parse the string as a JSON object/array and runs the SAME
|
||||
dict-level filtering on it. Non-JSON strings are returned unchanged.
|
||||
"""
|
||||
stripped = content.strip()
|
||||
if not stripped.startswith(("{", "[")):
|
||||
return content
|
||||
try:
|
||||
import json
|
||||
|
||||
parsed = json.loads(stripped)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return content # not JSON — plain string content is not a leak vector
|
||||
if isinstance(parsed, dict):
|
||||
filtered = _filter_dict_content(parsed, metadata, compliance, agent_definition)
|
||||
import json
|
||||
|
||||
return json.dumps(filtered)
|
||||
if isinstance(parsed, list):
|
||||
filtered = [
|
||||
_filter_dict_content(item, metadata, compliance, agent_definition)
|
||||
if isinstance(item, dict)
|
||||
else item
|
||||
for item in parsed
|
||||
]
|
||||
import json
|
||||
|
||||
return json.dumps(filtered)
|
||||
return content
|
||||
|
||||
|
||||
def _filter_dict_content(
|
||||
data: dict[str, Any],
|
||||
metadata: AIUseCaseMetadata,
|
||||
|
||||
Reference in New Issue
Block a user