@@ -166,7 +166,7 @@ public function completeWorkflowWithLongContent(): void
166166
167167 #[Test]
168168 #[DataProvider('xssPayloadProvider ' )]
169- public function completeWorkflowEscapesXssFromLlm (string $ maliciousContent , string $ description ): void
169+ public function completeWorkflowReturnsRawLlmContent (string $ maliciousContent , string $ description ): void
170170 {
171171 // Arrange: LLM returns malicious content (simulating prompt injection attack)
172172 $ llmResponse = $ this ->createOpenAiResponse (
@@ -183,15 +183,12 @@ public function completeWorkflowEscapesXssFromLlm(string $maliciousContent, stri
183183 $ request = $ this ->createJsonRequest (['prompt ' => 'Improve this text ' ]);
184184 $ result = $ stack ['controller ' ]->completeAction ($ request );
185185
186- // Assert: XSS content is escaped through the entire stack
186+ // Assert: Raw content returned in JSON (JSON encoding is inherently XSS-safe;
187+ // frontend sanitizes via DOMParser before DOM insertion)
187188 $ data = json_decode ((string ) $ result ->getBody (), true , 512 , JSON_THROW_ON_ERROR );
188189 self ::assertIsArray ($ data );
189190 self ::assertTrue ($ data ['success ' ]);
190-
191- // Verify HTML entities are used, not raw tags
192- self ::assertStringNotContainsString ('< ' , $ data ['content ' ], "Raw < found in: {$ description }" );
193- self ::assertStringNotContainsString ('> ' , $ data ['content ' ], "Raw > found in: {$ description }" );
194- self ::assertStringContainsString ('< ' , $ data ['content ' ], "Missing escaped < in: {$ description }" );
191+ self ::assertSame ($ maliciousContent , $ data ['content ' ], "Content mismatch for: {$ description }" );
195192 }
196193
197194 /**
@@ -226,9 +223,9 @@ public static function xssPayloadProvider(): iterable
226223 }
227224
228225 #[Test]
229- public function completeWorkflowEscapesXssInModelName (): void
226+ public function completeWorkflowReturnsRawModelName (): void
230227 {
231- // Arrange: LLM returns XSS in model field (edge case)
228+ // Arrange: LLM returns special chars in model field
232229 $ llmResponse = new CompletionResponse (
233230 content: 'Normal response ' ,
234231 model: '<script>alert("xss")</script> ' ,
@@ -246,11 +243,10 @@ public function completeWorkflowEscapesXssInModelName(): void
246243 $ request = $ this ->createJsonRequest (['prompt ' => 'Test ' ]);
247244 $ result = $ stack ['controller ' ]->completeAction ($ request );
248245
249- // Assert: Model field is also escaped
246+ // Assert: Raw content in JSON (JSON encoding is inherently safe)
250247 $ data = json_decode ((string ) $ result ->getBody (), true , 512 , JSON_THROW_ON_ERROR );
251248 self ::assertIsArray ($ data );
252- self ::assertStringNotContainsString ('<script> ' , $ data ['model ' ]);
253- self ::assertStringContainsString ('<script> ' , $ data ['model ' ]);
249+ self ::assertSame ('<script>alert("xss")</script> ' , $ data ['model ' ]);
254250 }
255251
256252 // =========================================================================
@@ -360,12 +356,12 @@ public function chatWorkflowEscapesXssInAllFields(): void
360356 ]);
361357 $ result = $ stack ['controller ' ]->chatAction ($ request );
362358
363- // Assert: All fields escaped
359+ // Assert: Raw content returned — frontend sanitizes before DOM insertion
364360 $ data = json_decode ((string ) $ result ->getBody (), true , 512 , JSON_THROW_ON_ERROR );
365361 self ::assertIsArray ($ data );
366- self ::assertStringNotContainsString ('< ' , $ data ['content ' ]);
367- self ::assertStringNotContainsString ('< ' , $ data ['model ' ]);
368- self ::assertStringNotContainsString ('< ' , $ data ['finishReason ' ]);
362+ self ::assertSame ('<script>steal()</script> ' , $ data ['content ' ]);
363+ self ::assertSame ('<img onerror=alert(1)> ' , $ data ['model ' ]);
364+ self ::assertSame ('<svg onload=hack()> ' , $ data ['finishReason ' ]);
369365 }
370366
371367 // =========================================================================
@@ -813,7 +809,7 @@ public function streamWorkflowReturnsChunkedSseResponse(): void
813809 }
814810
815811 #[Test]
816- public function streamWorkflowEscapesXssInChunks (): void
812+ public function streamWorkflowReturnsRawContentInChunks (): void
817813 {
818814 $ stack = $ this ->createStreamingStack (['<script>alert(1)</script> ' ]);
819815 $ config = $ this ->createLlmConfiguration ();
@@ -825,17 +821,16 @@ public function streamWorkflowEscapesXssInChunks(): void
825821 $ events = $ this ->parseSseEvents ((string ) $ result ->getBody ());
826822 self ::assertCount (2 , $ events ); // 1 content + 1 done
827823
828- // XSS content must be escaped
829- self ::assertStringNotContainsString ('<script> ' , $ events [0 ]['content ' ]);
830- self ::assertStringContainsString ('<script> ' , $ events [0 ]['content ' ]);
824+ // Raw content in JSON-encoded SSE data (JSON encoding is inherently safe)
825+ self ::assertSame ('<script>alert(1)</script> ' , $ events [0 ]['content ' ]);
831826 }
832827
833828 #[Test]
834- public function streamWorkflowEscapesXssInModelName (): void
829+ public function streamWorkflowReturnsRawModelName (): void
835830 {
836831 $ stack = $ this ->createStreamingStack (['Hello ' ]);
837832
838- // Create config with XSS in model name
833+ // Create config with special chars in model name
839834 $ config = self ::createStub (\Netresearch \NrLlm \Domain \Model \LlmConfiguration::class);
840835 $ config ->method ('getIdentifier ' )->willReturn ('test ' );
841836 $ config ->method ('getName ' )->willReturn ('Test ' );
@@ -849,8 +844,7 @@ public function streamWorkflowEscapesXssInModelName(): void
849844 $ events = $ this ->parseSseEvents ((string ) $ result ->getBody ());
850845 $ doneEvent = end ($ events );
851846 self ::assertTrue ($ doneEvent ['done ' ]);
852- self ::assertStringNotContainsString ('<img ' , $ doneEvent ['model ' ]);
853- self ::assertStringContainsString ('<img ' , $ doneEvent ['model ' ]);
847+ self ::assertSame ('<img onerror=alert(1)> ' , $ doneEvent ['model ' ]);
854848 }
855849
856850 #[Test]
@@ -999,7 +993,7 @@ public function getTasksWorkflowReturnsActiveTasks(): void
999993 }
1000994
1001995 #[Test]
1002- public function getTasksWorkflowEscapesXss (): void
996+ public function getTasksWorkflowReturnsRawContent (): void
1003997 {
1004998 $ stack = $ this ->createCompleteStack ([]);
1005999
@@ -1020,9 +1014,10 @@ public function getTasksWorkflowEscapesXss(): void
10201014 self ::assertIsArray ($ data );
10211015 self ::assertTrue ($ data ['success ' ]);
10221016 self ::assertCount (1 , $ data ['tasks ' ]);
1023- self ::assertStringNotContainsString ('<script> ' , $ data ['tasks ' ][0 ]['identifier ' ]);
1024- self ::assertStringNotContainsString ('<img ' , $ data ['tasks ' ][0 ]['name ' ]);
1025- self ::assertStringNotContainsString ('<svg ' , $ data ['tasks ' ][0 ]['description ' ]);
1017+ // Raw content — JSON encoding is the transport safety, frontend sanitizes
1018+ self ::assertSame ('<script>alert(1)</script> ' , $ data ['tasks ' ][0 ]['identifier ' ]);
1019+ self ::assertSame ('<img onerror=hack> ' , $ data ['tasks ' ][0 ]['name ' ]);
1020+ self ::assertSame ('<svg onload=steal()> ' , $ data ['tasks ' ][0 ]['description ' ]);
10261021 }
10271022
10281023 #[Test]
@@ -1226,7 +1221,7 @@ public function executeTaskWorkflowWithAdHocRules(): void
12261221 }
12271222
12281223 #[Test]
1229- public function executeTaskWorkflowEscapesXssFromLlm (): void
1224+ public function executeTaskWorkflowReturnsRawLlmContent (): void
12301225 {
12311226 $ llmResponse = $ this ->createOpenAiResponse (
12321227 content: '<script>document.cookie</script>Malicious response ' ,
@@ -1249,11 +1244,12 @@ public function executeTaskWorkflowEscapesXssFromLlm(): void
12491244 ]);
12501245 $ result = $ stack ['controller ' ]->executeTaskAction ($ request );
12511246
1247+ // Raw content in JSON (JSON encoding is inherently safe;
1248+ // frontend sanitizes via DOMParser before DOM insertion)
12521249 $ data = json_decode ((string ) $ result ->getBody (), true , 512 , JSON_THROW_ON_ERROR );
12531250 self ::assertIsArray ($ data );
12541251 self ::assertTrue ($ data ['success ' ]);
1255- self ::assertStringNotContainsString ('<script> ' , $ data ['content ' ]);
1256- self ::assertStringContainsString ('<script> ' , $ data ['content ' ]);
1252+ self ::assertSame ('<script>document.cookie</script>Malicious response ' , $ data ['content ' ]);
12571253 }
12581254
12591255 #[Test]
0 commit comments