OpenReplay: Integrations Make the Server Fetch Any Address

The Slack, Teams and webhook endpoints accept any address, check nothing and return the internal service's response.

Advisory ID: TP-2026-089
Product: OpenReplay (session replay and product analytics, Python and FastAPI)
Vulnerability type: Server-side request forgery (CWE-918)
CVE: requested (GitHub CNA), assignment pending
CVSS 3.1: 8.5 (High), our own assessment; the vendor advisory carries no rating
Affected versions: <= 1.27.0
Vendor advisory: GHSA-7h4c-j67v-5qm2
Fixed in: not stated unambiguously, the vendor advisory lists 1.27.0 as both affected and patched
Reported: 29 May 2026

Summary

OpenReplay lets users create integrations for Slack, Microsoft Teams and arbitrary webhooks, storing an address that the server then calls. Nowhere in the code is an internal, loopback or link-local address rejected, and the HTTP client follows redirects, so an allowlist of names would not contain the request either. The Slack integration fires its request as soon as it is created, and the status code reveals whether a port is open, closed or filtered. The path that posts a note to Slack returns the response body to the caller verbatim, turning the blind request into a reading one. All three routes require only an authenticated session in the lowest role, with no administrator rights.

Root cause

AddCollaborationSchema.url accepts any host (api/schemas/schemas.py:126), as does WebhookSchema.processed_endpoint (:190). When a Slack integration is created, say_hello immediately calls requests.post(url=url) with no timeout, no allow_redirects=False and no address validation (api/chalicelib/core/collaborations/collaboration_slack.py:31). The reading path returns the body as {"data": r.text} through send_raw (:68), and collaboration_msteams.py:71 behaves the same. The webhook branch calls requests.post(url=hook["endpoint"], ...) and stores r.text (api/chalicelib/core/webhook.py:175 and :187), while the matching routes depend on OR_context alone (api/routers/core_dynamic.py:165 and :528). A search of the shipped code for ipaddress, is_private, is_loopback, 169.254 or an allowlist returns nothing.

Proof of Concept

# as a member in the lowest role, no administrator rights needed:
POST /integrations/slack
{"name": "poc", "url": "http://169.254.169.254/latest/meta-data/"}

# the server fetches the address immediately on creation.
# then read through the note path:
POST /<project>/notes/<id>/slack/<integration-id>
-> {"data": "<the internal service's answer>"}

The status code returned on creation tells open, closed and filtered ports apart, which makes it a scanner for the internal network even without the reading path.

Impact

  • Retrieval of arbitrary internal HTTP services from the server's network by any authenticated member.
  • Disclosure of the responses in plain text through the note path, including the metadata service on a cloud deployment.
  • Port scanning of the internal network through the three distinguishable status codes.
  • No containment through an allowlist of names, since the client follows redirects.

References

Is Something Like This in Your Software?

Our team found this vulnerability in the course of its work. Have your applications tested by the same specialists, with a penetration test from turingpoint.