I was poking at a subdomain that looked like a product search assistant. The kind of internal tooling that gets deployed fast, tested for functionality, and never audited for security. A chatbot. Built on Rasa.

That turned out to be a mistake — for them.

Recon: Finding the Assistant

Nothing exotic in how this surface appeared. Subdomain enumeration flagged a *-assistant pattern on a cloud infrastructure domain. Resolved. Returned a 200. The response structure was immediately recognizable to anyone who's worked with open-source conversational AI frameworks: Rasa.

Rasa has a documented HTTP API. That's not a secret — it's in their official docs. What matters in production is whether someone actually configured authentication on it. The default answer is no. Most deployments don't.

First check: hit the tracker endpoint cold, no headers, no token:

curl -s -o /dev/null -w "%{http_code}" \
  "https://assistant.target.com/conversations/test_user/tracker"

200.

Not a 401. Not a 403. A clean 200 with a full JSON body.

The Vulnerability: IDOR Without a Lock

The Rasa HTTP API uses a sender_id parameter to identify conversations. The problem: it's entirely user-controlled, and the server performs zero ownership validation. There's no concept of "this sender_id belongs to this authenticated user." You supply a string, you get the conversation.

Three endpoints, all unauthenticated, all wide open:

GET  /conversations/{sender_id}/tracker         → Read full conversation state
POST /conversations/{sender_id}/tracker/events  → Write arbitrary events/slots
POST /conversations/{sender_id}/execute         → Execute registered actions

This isn't just read access. It's full CRUD on any conversation in the system.

Proving the IDOR: Cross-Session Isolation Failure

The PoC is almost embarrassingly simple:

Session A writes sensitive data:

curl -s -X POST \
  "https://assistant.target.com/conversations/session_A/tracker/events" \
  -H "Content-Type: application/json" \
  -d '[{"event":"slot","name":"dsl_query","value":"SECRET_FROM_SESSION_A"}]'

Session B reads it — no shared cookies, no auth, completely separate context:

curl -s \
  "https://assistant.target.com/conversations/session_A/tracker" \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['slots'].get('dsl_query'))"

Output:

SECRET_FROM_SESSION_A

Cross-session isolation: broken. Any attacker who knows or guesses a sender_id owns that conversation.

What Was Stored in Those Slots

The dsl_query slot was the critical one. This chatbot interfaced with an internal data catalog platform. Users were submitting search queries for internal assets — database schemas, internal asset names, structured catalog queries. All of it sitting in slots, readable by anyone.

Unauthenticated action execution made it worse:

curl -s -X POST \
  "https://assistant.target.com/conversations/test_user/execute" \
  -H "Content-Type: application/json" \
  -d '{"name":"action_search_assets_in_atlan","policy":"MemoizationPolicy","confidence":1}'

The /execute endpoint fired registered Rasa actions against any session without auth. One of those actions — action_search_assets_in_atlan — made authenticated backend calls to an internal data catalog platform. An unauthenticated external attacker could proxy requests through the chatbot into internal systems.

Information Disclosure: Error Messages and the Domain Endpoint

Triggering a broken action returned something useful:

{
  "messages": [{
    "text": "I'm sorry, I encountered an error. \n'ATLAN_API_KEY'"
  }]
}

The error message leaked the internal environment variable name. Confirmation the backend integration used a hardcoded or env-injected API key — and that key name was now public.

The /domain endpoint finished the job:

curl -s "https://assistant.target.com/domain" | jq '{slots: .slots, actions: .actions}'

Full Rasa configuration returned without auth: every slot name, every registered action, every response template, internal URLs including Slack channel links and whitelist service endpoints. Complete infrastructure mapping from a single unauthenticated GET.

Attack Chain

Identify *-assistant subdomain
       ↓
Hit /conversations/any_id/tracker → 200, no auth
       ↓
Read active sessions: extract dsl_query slot values
(internal catalog search queries exposed)
       ↓
Write to sessions: inject malicious slot values
(corrupt legitimate user sessions)
       ↓
/execute: trigger action_search_assets_in_atlan
(proxy unauthenticated requests into internal data catalog)
       ↓
/domain: enumerate full Rasa config
(actions, slots, internal URLs, Slack channels)
       ↓
Error messages: leak ATLAN_API_KEY variable name
(backend integration intelligence)

Impact

Vector Detail Conversation hijacking Read/write any user's full session state Internal data exposure Search queries for internal asset catalog Unauthenticated action execution Proxy requests into internal platform via /execute Infrastructure disclosure Internal service URLs, Slack channels, action names Secret variable disclosure ATLAN_API_KEY leaked in error responses Session data injection Overwrite slot values in any active conversation

The /execute endpoint chaining into an internal data platform is the critical path. This isn't just a chatbot — it's a proxy into internal infrastructure with no authentication gate.

Root Cause

Rasa's HTTP API ships with authentication disabled by default. The fix is one config line:

# endpoints.yml
rasa:
  auth_token: "your-strong-token-here"

But that only handles the authentication layer. The full remediation:

  1. Add auth_token to all Rasa API endpoints — non-negotiable in production
  2. Validate sender_id ownership — bind conversation IDs to authenticated user sessions; reject cross-session reads
  3. Sanitize error messages — strip internal variable names (ATLAN_API_KEY) from user-facing errors
  4. Restrict /domain — require authentication; this endpoint is an infrastructure map
  5. Rate limit enumerable endpoints — even with auth, sender_id patterns shouldn't be freely bruteforceable
  6. Non-predictable IDs — UUIDs generated server-side, not user-supplied strings

Key Takeaways

  • Rasa HTTP API has zero auth by default — if you see a Rasa deployment, test /conversations/{id}/tracker immediately
  • sender_id is fully user-controlled — it's not validated, not bound to sessions, not protected
  • Chatbot slots store sensitive data — search queries, user inputs, extracted entities all live here
  • /execute is the escalation path — firing backend actions through an unauthenticated endpoint is a proxy primitive
  • /domain is free recon — full action/slot/response enumeration without auth
  • Error messages talk — always trigger error states; internal variable names in stack traces are intelligence
  • Assistant/chatbot subdomains are under-audited — they're built for usability, not security
None
HackerOne report

Thanks for reading.

#BugBounty #IDOR #Rasa #ChatbotSecurity #APISecurity #InfoSec #Pentesting