Copilot commented on code in PR #13209:
URL: https://github.com/apache/cloudstack/pull/13209#discussion_r3978925534
##########
server/src/main/java/com/cloud/api/ApiServlet.java:
##########
@@ -216,6 +216,7 @@ void processRequestInContext(final HttpServletRequest req,
final HttpServletResp
HttpUtils.RESPONSE_TYPE_XML,
ApiServer.JSONcontentType.value());
return;
}
+ CallContext.current().setRequestRemoteAddress(req.getServerName());
Review Comment:
req.getServerName() is derived from the incoming Host header / request URL
and can be client-controlled in many deployments; persisting it into
CallContext and using it to build password-reset links can enable host-header
injection/phishing unless it is strictly validated/whitelisted (e.g., against
configured GUI theme CNs or a dedicated allowed domain list) and includes the
correct external scheme/port.
##########
server/src/main/java/org/apache/cloudstack/user/UserPasswordResetManagerImpl.java:
##########
@@ -182,26 +189,12 @@ public void setResetTokenAndSend(UserAccount userAccount)
{
final String email = userAccount.getEmail();
final String username = userAccount.getUsername();
final String subject = "Password Reset Request";
- String domainUrl = UserPasswordResetDomainURL.value();
- if (StringUtils.isBlank(domainUrl)) {
- String mgmtServerAddr =
ManagementServerAddresses.value().split(",")[0];
- if (ServerProperties.isHttpsEnabled()) {
- domainUrl = "https://" + mgmtServerAddr + ":" +
ServerProperties.getHttpsPort();
- } else {
- domainUrl = "http://" + mgmtServerAddr + ":" +
ServerProperties.getHttpPort();
- }
- } else if (!domainUrl.startsWith("http://") &&
!domainUrl.startsWith("https://")) {
- if (ServerProperties.isHttpsEnabled()) {
- domainUrl = "https://" + domainUrl;
- } else {
- domainUrl = "http://" + domainUrl;
- }
- }
-
- domainUrl = domainUrl.replaceAll("/+$", "");
+ String requestDomain = CallContext.current().getRequestRemoteAddress();
+ String resetLinkDomain = getResetLinkDomain(requestDomain);
+ String formattedResetLinkDomain =
formatResetLinkDomain(resetLinkDomain);
String resetLink =
String.format("%s/client/#/user/resetPassword?username=%s&token=%s",
- domainUrl, username, resetToken);
+ formattedResetLinkDomain, username, resetToken);
Review Comment:
`requestDomain` is sourced from CallContext’s `requestRemoteAddress`, which
is currently populated with `req.getServerName()` (host only, no port). When
the UI/API is accessed on a non-default port (e.g., :8080/:8443) without a
reverse proxy, the generated reset link will omit the port for hostname-based
domains, producing a broken link.
##########
api/src/main/java/org/apache/cloudstack/context/CallContext.java:
##########
@@ -66,6 +66,7 @@ protected Stack<CallContext> initialValue() {
private final Map<String, String> apiResourcesUuids = new HashMap<>();
private Project project;
private String apiName;
+ private String requestRemoteAddress;
Review Comment:
The new CallContext field name `requestRemoteAddress` is misleading:
ApiServlet sets it from `req.getServerName()` (the server/domain being
accessed), not the client remote address. This increases the risk of future
misuse (e.g., using it for auditing/security decisions) and makes the
password-reset flow harder to reason about.
##########
server/src/main/java/org/apache/cloudstack/user/UserPasswordResetManagerImpl.java:
##########
@@ -222,6 +215,44 @@ public void setResetTokenAndSend(UserAccount userAccount) {
userAccount, userAccount.getAccountId(),
userAccount.getDomainId(), email, resetTokenExpiryTime);
}
+ private String getResetLinkDomain(String requestDomain) {
+ if (StringUtils.isNotBlank(requestDomain)) {
+ logger.debug("Searching for GUI theme with common name that
matches the request's domain: [{}]", requestDomain);
+ List<Long> commonNameDetails =
guiThemeDetailsDao.listGuiThemeIdsByCommonName(requestDomain);
+
+ if (!commonNameDetails.isEmpty()) {
+ logger.debug("GUI theme with ID {} was found; using request's
domain for password reset link.", commonNameDetails.get(0));
+ return requestDomain;
+ } else {
+ logger.debug("No GUI theme was found with a common name that
matches the request's domain.");
+ }
+ }
+
+ String configurationDomain = UserPasswordResetDomainURL.value();
+ if (StringUtils.isNotBlank(configurationDomain)) {
+ logger.debug("Defaulting reset link's domain to the [{}]
configuration value: [{}].", UserPasswordResetDomainURL.key(),
UserPasswordResetDomainURL.value());
+ return configurationDomain;
+ }
+
+ logger.debug("Using the first IP address in the [{}] configuration for
the reset password email domain because the [{}] configuration is not
defined.", ManagementServerAddresses.key(), UserPasswordResetDomainURL.key());
+ return ManagementServerAddresses.value().split(",")[0];
+ }
+
+ private String formatResetLinkDomain(String resetLinkDomain) {
+ String protocol = ServerProperties.isHttpsEnabled() ? "https" : "http";
+
+ if (InetAddressUtils.isIPv4Address(resetLinkDomain)) {
+ int port = protocol.equals("https") ?
ServerProperties.getHttpsPort() : ServerProperties.getHttpPort();
+ resetLinkDomain = resetLinkDomain + ":" + port;
+ }
+
+ if (!resetLinkDomain.startsWith("http")) {
+ resetLinkDomain = protocol + "://" + resetLinkDomain;
+ }
+
+ return resetLinkDomain.replaceAll("/+$", "");
+ }
Review Comment:
The new domain-selection and formatting logic (`getResetLinkDomain` /
`formatResetLinkDomain`) introduces multiple branches (GUI theme match, config
fallback, host fallback, IPv4 vs hostname scheme handling) but there are no
unit tests covering these behaviors, even though this component already has a
dedicated test class.
This issue also appears in the following locations of the same file:
- line 220
- line 231
- line 249
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]