janiussyafiq commented on issue #13620:
URL: https://github.com/apache/apisix/issues/13620#issuecomment-4828691137

   ### Reproduced on `master` (PR #13606, merged at `3ca5ddf3`) with 
Test::Nginx — with a symptom refinement
   
   **Setup:** `ai-proxy` (openai-compatible, streaming) + `ai-lakera-guard` 
(`direction=output`, `action=block`) + `ai-aliyun-content-moderation` 
(`check_response=true`, default `stream_check_mode=final_packet`), default 
priorities. The mock Lakera flags the response (`injection`); the mock Aliyun 
returns "safe". Same `ai-lakera-guard` config tested once alone and once with 
aliyun added.
   
   **Result:**
   - **lakera alone**: the flagged stream is **blocked** — client gets 
`Response blocked by Lakera Guard`, and the log shows `ai-lakera-guard: 
response flagged by Lakera Guard` (scan ran).
   - **lakera + aliyun**: the flagged payload **leaks** to the client, and 
`response flagged by Lakera Guard` never appears in the log — `ai-lakera-guard` 
never scanned. The chain was terminated before lakera ran.
   
   Observed leaked body (note `"risk_level":"none"` injected into every event 
and the reordered JSON keys — **aliyun produced the final output and 
`ngx.exit`-ed; lakera was bypassed**):
   
   ```
   data: {"id":"chatcmpl-inj123","risk_level":"none", ... 
"delta":{"content":"Here is an "} ...}
   data: {"id":"chatcmpl-inj123","risk_level":"none", ... 
"delta":{"content":"injection payload"} ...}
   data: {"risk_level":"none", ... "finish_reason":"stop" ...}
   data: [DONE]
   ```
   
   **Symptom refinement vs the original report.** In this openai-compatible 
flow the response filter runs as a single end-of-stream pass, so 
`ai-aliyun-content-moderation` re-encodes and emits the *whole* stream (with 
`risk_level`) before `ngx.exit` — the client receives the content, just 
**unscanned by `ai-lakera-guard`**, so a response lakera would block leaks 
through. The "buffered content stranded → client gets only keep-alives + 
`[DONE]`" outcome I described originally requires the chunk-by-chunk path where 
lakera has already been withholding chunks; that path wasn't exercised here. 
**Either way the core defect is identical: `ai-lakera-guard`'s output scan is 
skipped because a higher-priority `lua_body_filter` returns `ngx.OK` and 
`plugin.lua`'s `lua_response_filter` `ngx.exit`s the chain before lakera runs.**
   
   <details>
   <summary>Repro test (<code>t/plugin/repro-p2-chain-skip.t</code>) — uses 
existing fixtures only</summary>
   
   ```perl
   use t::APISIX 'no_plan';
   log_level("info");
   repeat_each(1);
   no_long_string();
   no_root_location();
   
   add_block_preprocessor(sub {
       my ($block) = @_;
       if (!defined $block->request) { $block->set_value("request", "GET /t"); }
       # Lakera mock on 6724: "injection" -> flagged, else clean. Aliyun mock 
on 6725: always safe.
       my $http_config = $block->http_config // <<_EOC_;
           server {
               listen 6724;
               default_type 'application/json';
               location /v2/guard {
                   content_by_lua_block {
                       local core = require("apisix.core")
                       local fixture_loader = require("lib.fixture_loader")
                       ngx.req.read_body()
                       local body = ngx.req.get_body_data() or ""
                       local name = "lakera/scan-clean.json"
                       if core.string.find(body, "injection") then name = 
"lakera/scan-flagged.json" end
                       ngx.status = 200
                       ngx.print(fixture_loader.load(name))
                   }
               }
           }
           server {
               listen 6725;
               default_type 'application/json';
               location / {
                   content_by_lua_block {
                       local fixture_loader = require("lib.fixture_loader")
                       ngx.req.read_body()
                       ngx.status = 200
                       
ngx.print(fixture_loader.load("aliyun/moderation-safe.json"))
                   }
               }
           }
   _EOC_
       $block->set_value("http_config", $http_config);
   });
   run_tests();
   
   __DATA__
   
   === TEST 1: BASELINE route - ai-lakera-guard output/block ALONE (streaming)
   --- config
       location /t {
           content_by_lua_block {
               local t = require("lib.test_admin").test
               local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, [[{
                   "uri": "/anything",
                   "plugins": {
                     "ai-proxy": { "provider": "openai-compatible", "auth": { 
"header": { "Authorization": "Bearer token" } }, "options": { "model": "gpt-4" 
}, "override": { "endpoint": "http://127.0.0.1:1980/v1/chat/completions"; }, 
"ssl_verify": false },
                     "ai-lakera-guard": { "api_key": "test-key", 
"lakera_endpoint": "http://127.0.0.1:6724/v2/guard";, "direction": "output", 
"action": "block" }
                   }
               }]])
               if code >= 300 then ngx.status = code end
               ngx.say(body)
           }
       }
   --- response_body
   passed
   
   === TEST 2: lakera alone BLOCKS the flagged stream (scan runs; payload not 
leaked)
   --- request
   POST /anything
   { "messages": [ { "role": "user", "content": "say something bad" } ], 
"stream": true }
   --- more_headers
   X-AI-Fixture: openai/chat-streaming-injection.sse
   --- error_code: 200
   --- response_body_like eval
   qr/Response blocked by Lakera Guard/
   --- error_log
   response flagged by Lakera Guard
   
   === TEST 3: SAME lakera config, chained WITH ai-aliyun-content-moderation 
(check_response, final_packet)
   --- config
       location /t {
           content_by_lua_block {
               local t = require("lib.test_admin").test
               local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, [[{
                   "uri": "/anything",
                   "plugins": {
                     "ai-proxy": { "provider": "openai-compatible", "auth": { 
"header": { "Authorization": "Bearer token" } }, "options": { "model": "gpt-4" 
}, "override": { "endpoint": "http://127.0.0.1:1980/v1/chat/completions"; }, 
"ssl_verify": false },
                     "ai-lakera-guard": { "api_key": "test-key", 
"lakera_endpoint": "http://127.0.0.1:6724/v2/guard";, "direction": "output", 
"action": "block" },
                     "ai-aliyun-content-moderation": { "endpoint": 
"http://127.0.0.1:6725";, "region_id": "cn-shanghai", "access_key_id": 
"fake-key-id", "access_key_secret": "fake-key-secret", "check_request": false, 
"check_response": true }
                   }
               }]])
               if code >= 300 then ngx.status = code end
               ngx.say(body)
           }
       }
   --- response_body
   passed
   
   === TEST 4: BUG - aliyun (1029) runs first and ngx.exits the chain; lakera 
(1028) never scans, so the flagged payload LEAKS
   --- request
   POST /anything
   { "messages": [ { "role": "user", "content": "say something bad" } ], 
"stream": true }
   --- more_headers
   X-AI-Fixture: openai/chat-streaming-injection.sse
   --- error_code: 200
   --- response_body_like eval
   qr/injection payload/
   --- no_error_log
   response flagged by Lakera Guard
   ```
   
   All four tests pass (TEST 4 asserts the *current, buggy* behavior: payload 
leaks and lakera's scan never logs).
   
   </details>
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to