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 actionsThis 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_ACross-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:
- Add
auth_tokento all Rasa API endpoints — non-negotiable in production - Validate
sender_idownership — bind conversation IDs to authenticated user sessions; reject cross-session reads - Sanitize error messages — strip internal variable names (
ATLAN_API_KEY) from user-facing errors - Restrict
/domain— require authentication; this endpoint is an infrastructure map - Rate limit enumerable endpoints — even with auth, sender_id patterns shouldn't be freely bruteforceable
- 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}/trackerimmediately sender_idis 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
/executeis the escalation path — firing backend actions through an unauthenticated endpoint is a proxy primitive/domainis 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

Thanks for reading.
#BugBounty #IDOR #Rasa #ChatbotSecurity #APISecurity #InfoSec #Pentesting