dotCMS / dotCMS/core

Harden per-site authorization and site resolution on the /api/v1/ai/* endpoints

Open
#37,491 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

dotCMS : AI Team : Modernization Type : Task
Dominant language
Java
Stars
970
Forks
486
Avg merge
3d 33m
Merged PRs (30d)
170

Description

Description

Review of #37431 surfaced gaps that are shared by the already-shipped /api/v1/ai/* endpoints. None of these is remote-anonymous-exploitable — InitBuilder.anonAccess defaults to AnonymousAccess.NONE and WebResource.checkAnonymousPermissions (WebResource.java:369-385) 401s anonymous callers — but together they mean any authenticated user can spend an arbitrary site's LLM budget, on an arbitrary model, with no per-caller site check.

1. No per-caller site permission check when siteId is omitted. Every endpoint falls back to getCurrentHostNoThrow(request) (or getHost(request)HostWebAPIImpl.java:49-57), which calls getCurrentHost(request, null) and resolves the site as system user (HostWebAPIImpl.java:78-79). The caller's permissions are never consulted. Only the explicit-siteId path checks anything, via HostAPI.find(siteId, user, false) in AiHostResolver.java:75-80.

2. Silent cross-site config fallback. HostAPIImpl.resolveHostName falls back to findDefaultHost when the server name matches no site or alias (HostAPIImpl.java:140-144), and ConfigService.config then falls back to SYSTEM_HOST when the resolved site has no dotAI secrets (ConfigService.java:49-53). A client pointed at localhost, an unaliased hostname, or sitting behind a proxy that rewrites Host silently spends the default (or system) site's credentials and gets a 200.

3. Inconsistent model passthrough. CompletionsResource.resolveForm forces non-admins onto the site's configured model (CompletionsResource.java:366-369). TextResource.generateRequest passes form.model straight through with no such check (TextResource.java:105,116). Same subsystem, opposite policy — the second lets any authenticated user name an arbitrary model.

4. No cost accounting. No AI path carries a @RequestCost price, so the instance-wide RequestCostFilter backstop (web.xml:175-178, off by default) does not even register an LLM call — which parks a request thread and spends real money.

5. Unauthenticated static probes. ImageResource.java:46, EmbeddingsResource.java:66 and SearchResource.java:54 expose GET /test returning a constant {"type": …} map with no auth. Harmless, but undocumented dead weight.

Note that reading providerConfig as APILocator.systemUser() (ConfigService.java:44) is the normal dotCMS Apps design — the secret never leaves the server. The gap is authorization to use a site's credentials, not secret leakage.

Context: frontend-user access is intentional

TextResource:74, ImageResource:93, SearchResource:124,205 and CompletionsResource.resolveForm:360 pass requiredBackendUser(true).requiredFrontendUser(true), and WebResource.checkRolePermissions is any-of, not all-of (WebResource.java:428-441) — so a registered frontend user qualifies. This is intended: a site calling AI on behalf of a visitor is a supported use case, consistent with the AIViewTool / SearchTool / CompletionsTool viewtools.

That means locking these endpoints to backend users is not the fix — it would break a supported use case. It also means "READ on the resolved Host" is a near-no-op as a gate for frontend callers, since publicly delivered sites necessarily grant Host READ to CMS Anonymous / frontend roles (HostWebAPIImpl.checkHostPermission:110-120 runs with respectAnonPerms=true for exactly that reason). Site READ is still worth enforcing for a backend user passing an explicit siteId for a site they cannot see.

Goal

Retrofit /api/v1/ai/* onto the shared resolution + authorization component introduced by #37431, so both endpoint families make one authorization decision in one place instead of drifting the way TextResource and CompletionsResource already have.

Acceptance Criteria
  • Every /api/v1/ai/* resource obtains its AppConfig through the shared resolution component introduced in #37431 — no resource resolves a Host or calls ConfigService.INSTANCE.config(...) directly
  • Site resolution is strict: a site with no dotAI configuration returns an error, never another site's providerConfig; the HostAPIImpl:140-144 default-site and ConfigService:49-53 SYSTEM_HOST fallbacks no longer apply to these endpoints
  • An explicit siteId targeting a site the caller cannot READ returns 403 — uniform across every endpoint, not just the paths that happen to check today
  • Frontend-user access is preserved on text/generate, image/generate, search and completions — no behavioral regression for the viewtool / site-visitor use case
  • Non-admin callers cannot select an arbitrary model on any endpoint: TextResource adopts the model pinning CompletionsResource:366-369 already applies
  • Each AI endpoint carries a @RequestCost price in the remote-HTTP-round-trip band (~100)
  • The unauthenticated GET /test probes are removed, or documented and kept deliberately
  • Existing Postman / integration coverage for /api/v1/ai/* still green, plus new coverage for the strict-resolution and model-pinning behavior
  • Release note covering the behavior change, and the PR labeled for rollback safety per ROLLBACK_UNSAFE_CATEGORIES.md (API contract change)
Priority

Medium — a real gap, but not anonymously exploitable, and it should land after #37431 so both families share one resolver rather than two.

Additional Context

Out of scope: per-site / per-token AI spend quota. The instance-wide LeakyTokenBucket (RATE_LIMIT_ENABLED, off by default) meters node resource-time, not spend, so it is not a budget control. With frontend-user access intended, a quota is the only real answer to "a site member can spend your LLM budget" — it needs its own design and issue.

  • Surfaced during review of #37431 (/api/inference/v1), from @ihoffmann-dot's feedback on #37433
  • #37431 introduces the shared resolver this issue adopts, so it should land first
  • Sibling client-side work: #37433 (@dotcms/ai-sdk-provider)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the shared resolution component from #37431, then trace AiHostResolver.java, ConfigService.java, HostAPIImpl.java, and each /api/v1/ai resource. Compare model handling in CompletionsResource.java and TextResource.java, and inspect the existing Postman/integration coverage. Done means strict site authorization and resolution, consistent model and request-cost policies, deliberate /test probe handling, passing coverage, and the required release note.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
api, backend, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.