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]