Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/client/cypress/locators/AdminsSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export default {
smtpAppsmithMailUserNameInput: "[name='APPSMITH_MAIL_USERNAME']",
smtpAppsmithMailPasswordInput: "[name='APPSMITH_MAIL_PASSWORD']",
smtpAppsmithMailTestButton: "[data-testid='admin-settings-button']",
appsmithBaseUrlInput: "[name='APPSMITH_BASE_URL']",
addEmailGhostInput:
"[data-testid='admin-settings-tag-input'] .bp3-input-ghost",
// Static URL related locators
Expand Down
21 changes: 21 additions & 0 deletions app/client/src/ce/pages/AdminSettings/config/configuration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,26 @@ export const APPSMITH_REDIS_URL: Setting = {
"* Appsmith internally uses Redis for session storage. Change this to an external redis for clustering",
};

export const APPSMITH_BASE_URL: Setting = {
id: "APPSMITH_BASE_URL",
category: SettingCategories.CONFIGURATION,
controlType: SettingTypes.TEXTINPUT,
controlSubType: SettingSubtype.TEXT,
label: "Appsmith Base URL",
subText:
"* The base URL where Appsmith is accessible. This is required for password reset and email verification links to work correctly.",
placeholder: "https://appsmith.example.com",
validate: (value: string) => {
if (!value || value.trim() === "") {
return "This field cannot be empty";
}

if (!value.startsWith("http://") && !value.startsWith("https://")) {
return "Base URL must start with http:// or https://";
}
},
};

export const APPSMITH_POOL_SIZE_CONFIG: Setting = {
id: "connectionMaxPoolSize",
category: SettingCategories.CONFIGURATION,
Expand Down Expand Up @@ -136,6 +156,7 @@ export const config: AdminConfigType = {
settings: [
APPSMITH_DB_URL,
APPSMITH_REDIS_URL,
APPSMITH_BASE_URL,
APPSMITH_POOL_SIZE_CONFIG,
APPSMITH_ALLOWED_FRAME_ANCESTORS_SETTING,
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ public Mono<ResponseDTO<Boolean>> forgotPasswordRequest(
// username scraping, where the response of this API can prove whether an email has an account or not.
return service.forgotPasswordTokenGenerate(userPasswordDTO)
.defaultIfEmpty(true)
.onErrorReturn(true)
.thenReturn(new ResponseDTO<>(HttpStatus.OK, true));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.apache.hc.core5.net.WWWFormCodec;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.mongodb.core.query.UpdateDefinition;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
Expand Down Expand Up @@ -108,6 +109,34 @@ public class UserServiceCEImpl extends BaseService<UserRepository, User, String>
private final UserServiceHelper userPoliciesComputeHelper;
private final InstanceVariablesHelper instanceVariablesHelper;

@Value("${APPSMITH_BASE_URL:}")
private String appsmithBaseUrl;

/**
* Validates that the provided base URL matches APPSMITH_BASE_URL if it is configured.
* This prevents account takeover attacks by ensuring password reset and email verification
* links are only sent to the configured base URL when APPSMITH_BASE_URL is set.
* If APPSMITH_BASE_URL is not configured, validation is skipped to maintain backwards compatibility.
*
* @param providedBaseUrl The base URL from the request (typically from Origin header)
* @return Mono that completes successfully if validation passes or is skipped, or errors if validation fails
*/
private Mono<Void> validateBaseUrl(String providedBaseUrl) {
// If APPSMITH_BASE_URL is not configured, skip validation for backwards compatibility
if (!StringUtils.hasText(appsmithBaseUrl)) {
return Mono.empty();
}

// If APPSMITH_BASE_URL is configured, validate that Origin header matches it
if (!appsmithBaseUrl.equals(providedBaseUrl)) {
return Mono.error(new AppsmithException(
AppsmithError.GENERIC_BAD_REQUEST,
"Origin header does not match APPSMITH_BASE_URL configuration."));
}

return Mono.empty();
}

protected static final WebFilterChain EMPTY_WEB_FILTER_CHAIN = serverWebExchange -> Mono.empty();
private static final String FORGOT_PASSWORD_CLIENT_URL_FORMAT = "%s/user/resetPassword?token=%s";
private static final Pattern ALLOWED_ACCENTED_CHARACTERS_PATTERN = Pattern.compile("^[\\p{L} 0-9 .\'\\-]+$");
Expand Down Expand Up @@ -188,7 +217,15 @@ public Mono<Boolean> forgotPasswordTokenGenerate(ResetUserPasswordDTO resetUserP
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ORIGIN));
}

String email = resetUserPasswordDTO.getEmail();
// Validate Origin header against APPSMITH_BASE_URL
return validateBaseUrl(resetUserPasswordDTO.getBaseUrl()).then(Mono.defer(() -> {
String email = resetUserPasswordDTO.getEmail();
return processForgotPasswordTokenGeneration(email, resetUserPasswordDTO);
}));
}

private Mono<Boolean> processForgotPasswordTokenGeneration(
String email, ResetUserPasswordDTO resetUserPasswordDTO) {

// Create a random token to be sent out.
final String token = UUID.randomUUID().toString();
Expand Down Expand Up @@ -811,7 +848,15 @@ public Mono<Boolean> resendEmailVerification(
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ORIGIN));
}

String email = resendEmailVerificationDTO.getEmail();
// Validate Origin header against APPSMITH_BASE_URL
return validateBaseUrl(resendEmailVerificationDTO.getBaseUrl()).then(Mono.defer(() -> {
String email = resendEmailVerificationDTO.getEmail();
return processResendEmailVerification(email, resendEmailVerificationDTO, redirectUrl);
}));
}

private Mono<Boolean> processResendEmailVerification(
String email, ResendEmailVerificationDTO resendEmailVerificationDTO, String redirectUrl) {

// Create a random token to be sent out.
final String token = UUID.randomUUID().toString();
Expand Down