Gửi #979787: PublicCMS V5 (V5.202608.a and earlier); V4.0 line also supported open redirect (CWE-601)thông tin

tiêu đềPublicCMS V5 (V5.202608.a and earlier); V4.0 line also supported open redirect (CWE-601)
Mô tảAnonymous open redirect via the payment returnUrl guard bypass Anonymous open redirect via the payment returnUrl guard bypass Data flow: HTTP query parameter returnUrl at GET /tradePayment/pay → TradePaymentController.pay -> SafeConfigComponent.getSafeUrl/isUnSafeUrl bypass -> RedirectView Location AFFECTED INTERFACES | Interface | |---| | GET /tradePayment/pay?returnUrl=<value>&paymentId=<id> | | GET /user/verifyEmail?authToken=<value>&returnUrl=<value> | | POST /doLogout?returnUrl=<value> | | POST /place/save?path=<place-path>&returnUrl=<value> | | POST /doRegister?name=<user>&nickname=<nick>&password=<pw>&repassword=<pw>&returnUrl=<value> | | POST /user/update form nickname,returnUrl,_csrf | | POST /user/changePassword form oldpassword,password,repassword,returnUrl,_csrf | | POST /user/saveEmail form email,returnUrl,_csrf | Primary route detail: GET /tradePayment/pay?returnUrl=<value>&paymentId=<id> on the public storefront dispatch servlet (WebDispatcherServlet, no authentication): the route resolves the current site from the Host header and returns a 302 whose Location is built from the request parameter returnUrl. No session, credential or CSRF token is required, and the response is produced even when paymentId does not resolve to any existing payment (the entity lookup is skipped and the redirect is still returned). ROOT CAUSE Source (anonymous HTTP query parameter returnUrl): - publiccms-trade/src/main/java/com/publiccms/controller/web/trade/TradePaymentController.java:86-99 @RequestMapping(value = "pay") -> String returnUrl parameter -> line 89 returnUrl = safeConfigComponent.getSafeUrl(returnUrl, site, request.getContextPath()); Guard that is supposed to make the redirect safe: - publiccms-core/src/main/java/com/publiccms/logic/component/config/SafeConfigComponent.java:166-172 getSafeUrl(...) returns the caller value only when isUnSafeUrl(...) is false; otherwise it falls back to the site path. - SafeConfigComponent.java:174-194 isUnSafeUrl(...) only inspects URLs that contain :// (after replacing \ with /) or start with //. Every other string - including an absolute URL written in the *authority-less* form http:host:port/path - reaches the final } else { return false; } branch at line 191-193 and is therefore declared safe. - Browsers resolve http:host:port/path (a scheme followed directly by a host, without //) as the absolute URL http://host:port/path per the WHATWG URL standard; Spring's RedirectView (spring-webmvc 7.0.8, createTargetUrl) emits the value verbatim in Location because a redirect: view name that does not start with / is not rewritten. Sink (server-issued redirect with an attacker-controlled destination): - TradePaymentController.java:99 returns CommonUtils.joinString(UrlBasedViewResolver.REDIRECT_URL_PREFIX, returnUrl), i.e. the view name redirect:<attacker-supplied value>, which Spring MVC renders as a 302 with Location: <attacker-supplied value>. Observed consequence: an anonymous caller controls the destination of the 302 returned to any visitor of a crafted link. The guard blocks https://attacker/... and //attacker/..., but not the authority-less http:attacker/... form, so the documented "safe return URL" control is bypassed. Intended function: returnUrl exists to send the shopper back to a site page after the payment step; redirecting an anonymous caller's visitor to an arbitrary external origin is not that documented purpose, and SECURITY.md contains no exclusion for it. PER-ROUTE DELTAS - Anonymous open redirect on the public e-mail verification route - HTTP query parameters authToken and returnUrl at GET /user/verifyEmail → UserController.verifyEmail -> SafeConfigComponent.getSafeUrl/isUnSafeUrl bypass -> RedirectView Location - Anonymous open redirect on the public logout route - HTTP form/query parameters userId and returnUrl at POST /doLogout → LoginController.logout -> SafeConfigComponent.getSafeUrl/isUnSafeUrl bypass -> RedirectView Location - Anonymous open redirect on the public place-contribution route - HTTP form/query parameters path and returnUrl at POST /place/save → PlaceController.save error branch -> SafeConfigComponent.getSafeUrl/isUnSafeUrl bypass -> RedirectView Location - Anonymous open redirect on the public registration route - HTTP form/query parameters name, nickname, password, repassword and returnUrl at POST /doRegister → LoginController.register -> SafeConfigComponent.getSafeUrl/isUnSafeUrl bypass -> RedirectView Location (final redirect after registration) - Authenticated profile-update open redirect bypassing the returnUrl guard - HTTP form field returnUrl at POST /user/update (ordinary site-user session, _csrf token) → SafeConfigComponent.getSafeUrl/isUnSafeUrl -> redirect: view -> 302 Location (both validation-error and success branches) - Authenticated password-change open redirect bypassing the returnUrl guard - HTTP form field returnUrl at POST /user/changePassword (ordinary site-user session, _csrf token) → SafeConfigComponent.getSafeUrl/isUnSafeUrl -> redirect: view -> 302 Location (mismatch branch and success branch) - Authenticated e-mail-change open redirect bypassing the returnUrl guard - HTTP form field returnUrl at POST /user/saveEmail (ordinary site-user session, _csrf token) → SafeConfigComponent.getSafeUrl/isUnSafeUrl -> redirect: view -> 302 Location (validation-error and success branches) PROOF OF CONCEPT Recorded on the exact audited baseline (war sha256 64cc07e1...a825de, HEAD b4c7f6b6...a9bd) with a real browser-equivalent client and a local listener on a test-only bridge network; no third-party system was contacted. The site was served over http on 127.0.0.1:18080; the listener answered on http://redirect-canary:18999. 1. Control - the guarded form (://) is rewritten to a site path: $ curl -s -i 'http://127.0.0.1:18080/tradePayment/pay?returnUrl=https://attacker.invalid/x&paymentId=999' HTTP/1.1 302 Location: http://127.0.0.1:18080/ 2. Attack - the authority-less form is emitted verbatim: $ curl -s -i 'http://127.0.0.1:18080/tradePayment/pay?returnUrl=http:redirect-canary:18999/return-here&paymentId=999' HTTP/1.1 302 Location: http:redirect-canary:18999/return-here 3. Follow-up by the visitor's browser - the Location value resolves to http://redirect-canary:18999/return-here (confirmed with a headless browser and independently with the WHATWG URL parser), and the listener logged the navigation: $ cat canary-requests.jsonl {"method": "GET", "path": "/return-here", "host": "redirect-canary:18999"} The guard rejects values containing :// and values starting with //, but a value of the form scheme:host[:port][/path] (no //) passes through unchanged and is resolved by the browser as an absolute URL. GET /tradePayment/pay is anonymous: no session and no CSRF token are required, and the redirect is returned even for a non-existent paymentId. IMPACT See the interface and root-cause sections: an actor who satisfies the stated preconditions makes the server perform a request or operation on a destination/target of the attacker's choosing, crossing the trust boundary described above. - Project repository: https://github.com/sanluan/PublicCMS - Security policy: `SECURITY.md` in that repository. - **Vendor contact**: PublicCMS maintainer - [email protected] - **Status**: vendor notified 2026-09-14 by email to [email protected]; thread subject: `Open redirect in PublicCMS via an authority-less URL that bypasses SafeConfigComponent`; not publicly disclosed; no CVE assigned - **Duplicate check**: public advisories (NVD/OSV/GitHub) and the project issue tracker were searched before reporting; please tell us if this duplicates something already tracked internally.
Người dùng
 lucasg2g (UID 84737)
Đệ trình15/09/2026 09:21 (cách đây 28 ngày)
Kiểm duyệt12/10/2026 14:10 (27 days later)
Trạng tháiđược chấp nhận
Mục VulDB416891 [PublicCMS đến 5.202608.a pay Endpoint SafeConfigComponent.java SafeConfigComponent.getSafeUrl returnUrl Redirect]
điểm17

Do you need the next level of professionalism?

Upgrade your account now!