fix: require explicit anonymous webhook access (#14890)

### What problem does this PR solve?

Fixes #14882

Agent webhook execution currently fails open when the saved webhook
`security` block is missing/empty, or when `auth_type` is set to `none`.
This allows unauthenticated webhook invocation without an explicit
operator opt-in.

This PR makes anonymous webhook access explicit:
- Rejects missing or empty webhook security config.
- Requires `allow_anonymous: true` when `auth_type` is `none`.
- Preserves explicit anonymous webhooks by having the frontend serialize
`allow_anonymous: true` when the user selects `None` auth.
- Updates webhook unit tests to cover both denied implicit-anonymous
configs and allowed explicit-anonymous configs.

### Type of change

- [x] Bug Fix
- [x] Security hardening
- [x] Test

### Tests

- [x] `ZHIPU_AI_API_KEY=dummy uv run python -m pytest
--confcutdir=test/testcases/test_web_api/test_agent_app
test/testcases/test_web_api/test_agent_app/test_agents_webhook_unit.py`
- [x] `uv run ruff check api/apps/restful_apis/agent_api.py
test/testcases/test_web_api/test_agent_app/test_agents_webhook_unit.py`
- [x] `npm exec eslint src/pages/agent/utils.ts
src/pages/agent/form/begin-form/schema.ts`

---------

Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
This commit is contained in:
Renzo
2026-06-27 19:20:29 -10:00
committed by yzc
parent 43a9d53c72
commit 6079ded70b
6 changed files with 191 additions and 32 deletions

View File

@@ -2881,6 +2881,9 @@ Important structured information may include: names, dates, locations, events, k
'Accepted Response: The system returns an acknowledgment immediately after the request is validated, while the workflow continues to execute asynchronously in the background. /Final Response: The system returns a response only after the workflow execution is completed.',
authMethods: 'Authentication methods',
authType: 'Authentication type',
allowAnonymous: 'Allow anonymous access',
allowAnonymousTip:
'Anyone with this webhook URL can trigger the agent when this is enabled.',
limit: 'Request frequency limit',
per: 'Time period',
maxBodySize: 'Maximum body size',

View File

@@ -29,6 +29,7 @@ export const BeginFormSchema = z.object({
per: z.string().optional(),
}),
max_body_size: z.string(),
allow_anonymous: z.boolean().optional(),
jwt: z
.object({
algorithm: z.string().default(WebhookJWTAlgorithmList[0]).optional(),

View File

@@ -1,6 +1,7 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { WebhookJWTAlgorithmList } from '@/constants/agent';
import { WebhookSecurityAuthType } from '@/pages/agent/constant';
import { buildOptions } from '@/utils/form';
@@ -100,7 +101,21 @@ export function Auth() {
[WebhookSecurityAuthType.Token]: renderTokenAuth,
[WebhookSecurityAuthType.Basic]: renderBasicAuth,
[WebhookSecurityAuthType.Jwt]: renderJwtAuth,
[WebhookSecurityAuthType.None]: () => null,
[WebhookSecurityAuthType.None]: () => (
<RAGFlowFormItem
name="security.allow_anonymous"
label={t('flow.webhook.allowAnonymous')}
tooltip={t('flow.webhook.allowAnonymousTip')}
horizontal
>
{(field) => (
<Switch
checked={field.value}
onCheckedChange={field.onChange}
></Switch>
)}
</RAGFlowFormItem>
),
};
return (

View File

@@ -456,6 +456,14 @@ function transformBeginParams(params: BeginFormSchemaType) {
required_claims: security?.jwt?.required_claims.map((x) => x.value),
};
}
if (
params.security?.auth_type === WebhookSecurityAuthType.None &&
params.security?.allow_anonymous
) {
nextSecurity.allow_anonymous = true;
} else {
delete nextSecurity.allow_anonymous;
}
return {
...params,
schema: transformRequestSchemaToJsonschema(params.schema),