The resolver MUST NOT trust request-supplied values such as the {@code Origin} + * header as the canonical host. Doing so allows an unauthenticated attacker to + * influence the link host of security-sensitive emails — see + * GHSA-j9gf-vw2f-9hrw. + * + *
Implementations return {@link Mono#empty()} when no trusted base URL can be + * resolved and the insecure compatibility flag is off. Callers are expected to + * translate this into a flow-appropriate response: + *
The CE implementation returns {@code true} iff {@code APPSMITH_BASE_URL} is set; + * the EE override returns {@code true} unconditionally when the multi-org feature flag + * is on (each organization derives its own canonical URL from slug + deploymentDomain). + * + *
Drives the in-product admin warning banner shown to instance super-users. The
+ * insecure-flag fallback intentionally does NOT mark the instance as healthy — operators
+ * who opted into the deprecated {@code APPSMITH_ALLOW_INSECURE_ORIGIN_BASED_LINKS} escape
+ * hatch should still see the warning so the deprecation pressure is preserved.
+ *
+ * @return {@code Mono} emitting {@code true} when no banner should be shown,
+ * {@code false} when the banner should warn that token-bearing emails are disabled.
+ */
+ Mono Resolution rules:
+ *
+ * Returns {@code false} for any malformed URL or null/blank input. Userinfo, path, query,
+ * and fragment are deliberately ignored — only the security-relevant origin is compared.
+ */
+ private static boolean sameOrigin(String configured, String provided) {
+ if (!StringUtils.hasText(configured) || !StringUtils.hasText(provided)) {
+ return false;
+ }
+ try {
+ URI configuredUri = new URI(configured.trim());
+ URI providedUri = new URI(provided.trim());
+
+ String configuredScheme = lowerCase(configuredUri.getScheme());
+ String providedScheme = lowerCase(providedUri.getScheme());
+ if (!Objects.equals(configuredScheme, providedScheme)) {
+ return false;
+ }
+
+ String configuredHost = lowerCase(configuredUri.getHost());
+ String providedHost = lowerCase(providedUri.getHost());
+ if (configuredHost == null || !Objects.equals(configuredHost, providedHost)) {
+ // Reject when host can't be parsed (e.g. opaque URIs) or hosts differ.
+ return false;
+ }
+
+ return effectivePort(configuredUri) == effectivePort(providedUri);
+ } catch (URISyntaxException e) {
+ log.warn("Failed to parse URL for origin comparison: {}", e.getMessage());
+ return false;
+ }
+ }
+
+ private static int effectivePort(URI uri) {
+ int port = uri.getPort();
+ if (port != -1) {
+ return port;
+ }
+ String scheme = lowerCase(uri.getScheme());
+ if ("http".equals(scheme)) {
+ return 80;
+ }
+ if ("https".equals(scheme)) {
+ return 443;
+ }
+ return -1;
+ }
+
+ private static String lowerCase(String value) {
+ return value == null ? null : value.toLowerCase(Locale.ROOT);
+ }
+}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ConsolidatedAPIServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ConsolidatedAPIServiceImpl.java
index 437a8287ff80..bea886725833 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ConsolidatedAPIServiceImpl.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ConsolidatedAPIServiceImpl.java
@@ -4,6 +4,7 @@
import com.appsmith.server.actioncollections.base.ActionCollectionService;
import com.appsmith.server.applications.base.ApplicationService;
import com.appsmith.server.datasources.base.DatasourceService;
+import com.appsmith.server.helpers.SecureBaseUrlResolver;
import com.appsmith.server.jslibs.base.CustomJSLibService;
import com.appsmith.server.newactions.base.NewActionService;
import com.appsmith.server.newpages.base.NewPageService;
@@ -39,7 +40,8 @@ public ConsolidatedAPIServiceImpl(
MockDataService mockDataService,
ObservationRegistry observationRegistry,
CacheableRepositoryHelper cacheableRepositoryHelper,
- ObservationHelper observationHelper) {
+ ObservationHelper observationHelper,
+ SecureBaseUrlResolver secureBaseUrlResolver) {
super(
sessionUserService,
userService,
@@ -59,6 +61,7 @@ public ConsolidatedAPIServiceImpl(
mockDataService,
observationRegistry,
cacheableRepositoryHelper,
- observationHelper);
+ observationHelper,
+ secureBaseUrlResolver);
}
}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/EmailServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/EmailServiceImpl.java
index 7e4d440d3ac2..0d6fe04f1991 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/EmailServiceImpl.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/EmailServiceImpl.java
@@ -1,13 +1,17 @@
package com.appsmith.server.services;
import com.appsmith.server.helpers.EmailServiceHelper;
+import com.appsmith.server.helpers.SecureBaseUrlResolver;
import com.appsmith.server.notifications.EmailSender;
import com.appsmith.server.services.ce.EmailServiceCEImpl;
import org.springframework.stereotype.Service;
@Service
public class EmailServiceImpl extends EmailServiceCEImpl implements EmailService {
- public EmailServiceImpl(EmailSender emailSender, EmailServiceHelper emailServiceHelper) {
- super(emailSender, emailServiceHelper);
+ public EmailServiceImpl(
+ EmailSender emailSender,
+ EmailServiceHelper emailServiceHelper,
+ SecureBaseUrlResolver secureBaseUrlResolver) {
+ super(emailSender, emailServiceHelper, secureBaseUrlResolver);
}
}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java
index 75d59671c003..6c8683fb4cf4 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java
@@ -2,6 +2,7 @@
import com.appsmith.server.configurations.CommonConfig;
import com.appsmith.server.configurations.EmailConfig;
+import com.appsmith.server.helpers.SecureBaseUrlResolver;
import com.appsmith.server.helpers.UserServiceHelper;
import com.appsmith.server.helpers.UserUtils;
import com.appsmith.server.instanceconfigs.helpers.InstanceVariablesHelper;
@@ -44,7 +45,8 @@ public UserServiceImpl(
RateLimitService rateLimitService,
PACConfigurationService pacConfigurationService,
UserServiceHelper userServiceHelper,
- InstanceVariablesHelper instanceVariablesHelper) {
+ InstanceVariablesHelper instanceVariablesHelper,
+ SecureBaseUrlResolver secureBaseUrlResolver) {
super(
validator,
repository,
@@ -62,6 +64,7 @@ public UserServiceImpl(
rateLimitService,
pacConfigurationService,
userServiceHelper,
- instanceVariablesHelper);
+ instanceVariablesHelper,
+ secureBaseUrlResolver);
}
}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceCEImpl.java
index 4f579b36e018..ee2609360250 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceCEImpl.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceCEImpl.java
@@ -12,6 +12,7 @@
import com.appsmith.server.domains.ApplicationMode;
import com.appsmith.server.domains.GitArtifactMetadata;
import com.appsmith.server.domains.NewPage;
+import com.appsmith.server.domains.OrganizationConfiguration;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.dtos.ApplicationPagesDTO;
import com.appsmith.server.dtos.ConsolidatedAPIResponseDTO;
@@ -21,6 +22,7 @@
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.GitUtils;
+import com.appsmith.server.helpers.SecureBaseUrlResolver;
import com.appsmith.server.helpers.TextUtils;
import com.appsmith.server.jslibs.base.CustomJSLibService;
import com.appsmith.server.newactions.base.NewActionService;
@@ -128,6 +130,7 @@ public class ConsolidatedAPIServiceCEImpl implements ConsolidatedAPIServiceCE {
private final ObservationRegistry observationRegistry;
private final CacheableRepositoryHelper cacheableRepositoryHelper;
private final ObservationHelper observationHelper;
+ private final SecureBaseUrlResolver secureBaseUrlResolver;
protected In single-org (CE) mode:
- * This method can be overridden in EE to handle multi-org setups where each organization
- * has its own base URL.
- *
- * @param providedBaseUrl The base URL from the request (typically from Origin header)
- * @return Mono These tests pin the fail-closed semantics added for
+ * GHSA-j9gf-vw2f-9hrw:
+ * when {@code APPSMITH_BASE_URL} is unset, the resolver must NOT trust the request-supplied
+ * {@code Origin} value as the host of token-bearing email links. It must signal "no trusted
+ * URL available" via {@link reactor.core.publisher.Mono#empty()} so that callers in unauthenticated
+ * flows return generic success without dispatching email (preserving anti-enumeration), while
+ * callers in authenticated flows can convert the empty into an explicit configuration error.
+ *
+ * The opt-in {@code APPSMITH_ALLOW_INSECURE_ORIGIN_BASED_LINKS} compatibility flag exists
+ * solely to give operators a migration window. When enabled, it restores the legacy behavior
+ * of trusting the caller-supplied value — but it does NOT weaken the strict-mode check that
+ * applies once {@code APPSMITH_BASE_URL} is configured.
+ */
+class SecureBaseUrlResolverCEImplTest {
+
+ private SecureBaseUrlResolverCEImpl newResolver(String configuredBaseUrl, boolean allowInsecureFallback)
+ throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = new SecureBaseUrlResolverCEImpl();
+ Field baseUrlField = SecureBaseUrlResolverCEImpl.class.getDeclaredField("appsmithBaseUrl");
+ baseUrlField.setAccessible(true);
+ baseUrlField.set(resolver, configuredBaseUrl == null ? "" : configuredBaseUrl);
+ Field flagField = SecureBaseUrlResolverCEImpl.class.getDeclaredField("allowInsecureOriginBasedLinks");
+ flagField.setAccessible(true);
+ flagField.set(resolver, allowInsecureFallback);
+ return resolver;
+ }
+
+ /**
+ * GHSA-j9gf-vw2f-9hrw — the central regression test. When the trusted base URL is unset and
+ * the insecure compatibility flag is off (the new default), the resolver must return an
+ * empty signal — NOT the caller-supplied value.
+ */
+ @Test
+ void resolveSecureBaseUrl_whenAppsmithBaseUrlUnsetAndCompatFlagOff_returnsEmpty() throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = newResolver("", false);
+
+ StepVerifier.create(resolver.resolveSecureBaseUrl("https://attacker.example"))
+ .verifyComplete();
+ }
+
+ /**
+ * Migration path: when an operator opts into the insecure flag during a transition window,
+ * the legacy behavior is restored — the caller-supplied value is returned. The WARN log is
+ * the operational signal that this is happening; we do not assert on the log here, but the
+ * production code MUST emit it (manual code review).
+ */
+ @Test
+ void resolveSecureBaseUrl_whenAppsmithBaseUrlUnsetAndCompatFlagOn_returnsProvidedValue() throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = newResolver("", true);
+
+ StepVerifier.create(resolver.resolveSecureBaseUrl("https://attacker.example"))
+ .expectNext("https://attacker.example")
+ .verifyComplete();
+ }
+
+ @Test
+ void resolveSecureBaseUrl_whenAppsmithBaseUrlSetAndOriginMatches_returnsConfiguredUrl() throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", false);
+
+ StepVerifier.create(resolver.resolveSecureBaseUrl("https://appsmith.example"))
+ .expectNext("https://appsmith.example")
+ .verifyComplete();
+ }
+
+ /**
+ * Regression on the protection added in PR #41426 (GHSA-7hf5-mc28-xmcv): once configured,
+ * the trusted base URL must not be impersonated.
+ */
+ @Test
+ void resolveSecureBaseUrl_whenAppsmithBaseUrlSetAndOriginMismatches_errors() throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", false);
+
+ StepVerifier.create(resolver.resolveSecureBaseUrl("https://attacker.example"))
+ .verifyError(AppsmithException.class);
+ }
+
+ /**
+ * Defense-in-depth: the insecure-compat flag is intended for the unset-config case only.
+ * It must NOT be a backdoor that weakens strict-mode validation when APPSMITH_BASE_URL is
+ * configured.
+ */
+ @Test
+ void resolveSecureBaseUrl_whenAppsmithBaseUrlSetAndOriginMismatches_compatFlagDoesNotWeakenStrictMode()
+ throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", true);
+
+ StepVerifier.create(resolver.resolveSecureBaseUrl("https://attacker.example"))
+ .verifyError(AppsmithException.class);
+ }
+
+ /**
+ * Empty Origin from the request, unset config, default fail-closed: empty signal.
+ * Sanity-check that the resolver does not blow up on null/empty caller-supplied values.
+ */
+ @Test
+ void resolveSecureBaseUrl_whenProvidedBaseUrlIsNullOrBlank_andCompatFlagOff_returnsEmpty() throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = newResolver("", false);
+
+ StepVerifier.create(resolver.resolveSecureBaseUrl(null)).verifyComplete();
+ StepVerifier.create(resolver.resolveSecureBaseUrl("")).verifyComplete();
+ }
+
+ @Test
+ void resolveSecureBaseUrl_constructed_isNotNull() throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", false);
+ assertNotNull(resolver);
+ }
+
+ // region URL-origin normalisation — comparison must follow RFC 6454 (scheme + host + effective port),
+ // not raw string equality. Without this, real-world deployments hit spurious mismatches whenever the
+ // configured value and the inbound `Origin` header differ on insignificant syntax (trailing slash,
+ // default-port elision, host case). All accepted matches below resolve to the SAME origin per the
+ // RFC; all rejected ones genuinely differ in scheme, host, or non-default port.
+
+ @Test
+ void resolveSecureBaseUrl_whenConfiguredHasTrailingSlash_andOriginDoesNot_match() throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = newResolver("http://localhost/", false);
+
+ StepVerifier.create(resolver.resolveSecureBaseUrl("http://localhost"))
+ .expectNext("http://localhost/")
+ .verifyComplete();
+ }
+
+ @Test
+ void resolveSecureBaseUrl_whenOriginHasTrailingSlash_andConfiguredDoesNot_match() throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = newResolver("http://localhost", false);
+
+ StepVerifier.create(resolver.resolveSecureBaseUrl("http://localhost/"))
+ .expectNext("http://localhost")
+ .verifyComplete();
+ }
+
+ @Test
+ void resolveSecureBaseUrl_whenOriginHasDefaultHttpPort_andConfiguredOmitsIt_match() throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = newResolver("http://localhost", false);
+
+ StepVerifier.create(resolver.resolveSecureBaseUrl("http://localhost:80"))
+ .expectNext("http://localhost")
+ .verifyComplete();
+ }
+
+ @Test
+ void resolveSecureBaseUrl_whenOriginHasDefaultHttpsPort_andConfiguredOmitsIt_match() throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", false);
+
+ StepVerifier.create(resolver.resolveSecureBaseUrl("https://appsmith.example:443"))
+ .expectNext("https://appsmith.example")
+ .verifyComplete();
+ }
+
+ @Test
+ void resolveSecureBaseUrl_whenHostCasingDiffers_match() throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = newResolver("https://Appsmith.Example", false);
+
+ StepVerifier.create(resolver.resolveSecureBaseUrl("https://appsmith.example"))
+ .expectNext("https://Appsmith.Example")
+ .verifyComplete();
+ }
+
+ @Test
+ void resolveSecureBaseUrl_whenSchemesDiffer_errors() throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", false);
+
+ StepVerifier.create(resolver.resolveSecureBaseUrl("http://appsmith.example"))
+ .verifyError(AppsmithException.class);
+ }
+
+ @Test
+ void resolveSecureBaseUrl_whenNonDefaultPortsDiffer_errors() throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = newResolver("http://localhost:8080", false);
+
+ StepVerifier.create(resolver.resolveSecureBaseUrl("http://localhost:9090"))
+ .verifyError(AppsmithException.class);
+ }
+
+ @Test
+ void resolveSecureBaseUrl_whenOriginIsMalformed_errors() throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", false);
+
+ StepVerifier.create(resolver.resolveSecureBaseUrl("not a url")).verifyError(AppsmithException.class);
+ }
+
+ @Test
+ void resolveSecureBaseUrl_whenAttackerUsesUserinfoTrick_errors() throws Exception {
+ // Tricks like https://appsmith.example@evil.com must NOT be accepted as the same origin
+ // as https://appsmith.example. URI parsing places appsmith.example in userinfo and evil.com
+ // as the host — so the host comparison rejects.
+ SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", false);
+
+ StepVerifier.create(resolver.resolveSecureBaseUrl("https://appsmith.example@evil.example"))
+ .verifyError(AppsmithException.class);
+ }
+
+ // endregion
+
+ // region isBaseUrlConfigurationHealthy — instance-config health signal driving the admin
+ // warning banner. The signal answers "can this instance generate token-bearing email links
+ // without depending on a request-time hint?". CE semantics: true iff APPSMITH_BASE_URL is
+ // set. The insecure-flag fallback intentionally does NOT mark the instance as healthy —
+ // operators who opted into the deprecated escape hatch should still see the warning so the
+ // deprecation pressure is preserved.
+
+ @Test
+ void isBaseUrlConfigurationHealthy_returnsTrueWhenBaseUrlSet() throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", false);
+
+ StepVerifier.create(resolver.isBaseUrlConfigurationHealthy())
+ .expectNext(true)
+ .verifyComplete();
+ }
+
+ @Test
+ void isBaseUrlConfigurationHealthy_returnsFalseWhenBaseUrlBlank() throws Exception {
+ SecureBaseUrlResolverCEImpl resolver = newResolver("", false);
+
+ StepVerifier.create(resolver.isBaseUrlConfigurationHealthy())
+ .expectNext(false)
+ .verifyComplete();
+ }
+
+ // endregion
+}
diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceImplTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceImplTest.java
index 8c3c7eeddec2..75096632618f 100644
--- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceImplTest.java
+++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceImplTest.java
@@ -34,6 +34,7 @@
import com.appsmith.server.dtos.UserProfileDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
+import com.appsmith.server.helpers.SecureBaseUrlResolver;
import com.appsmith.server.jslibs.base.CustomJSLibService;
import com.appsmith.server.newactions.base.NewActionService;
import com.appsmith.server.newpages.base.NewPageService;
@@ -50,6 +51,7 @@
import com.appsmith.server.services.UserDataService;
import com.appsmith.server.services.UserService;
import com.appsmith.server.themes.base.ThemeService;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
@@ -103,6 +105,23 @@ public class ConsolidatedAPIServiceImplTest {
@MockBean
ProductAlertService mockProductAlertService;
+ /**
+ * GHSA-j9gf-vw2f-9hrw — the consolidated-api now calls
+ * {@link SecureBaseUrlResolver#isBaseUrlConfigurationHealthy()} alongside the org-config
+ * fetch. Mock it here so the test class doesn't escape into the real EE resolver chain
+ * (which transitively pulls in FeatureFlagService → OrganizationService and would NPE
+ * since OrganizationService is itself a @MockBean returning null for unstubbed methods).
+ */
+ @MockBean
+ SecureBaseUrlResolver mockSecureBaseUrlResolver;
+
+ @BeforeEach
+ void stubSecureBaseUrlResolver() {
+ // Default to healthy=true. Existing tests don't care about the admin-banner state;
+ // dedicated tests for the banner signal stub their own override on this mock.
+ when(mockSecureBaseUrlResolver.isBaseUrlConfigurationHealthy()).thenReturn(Mono.just(true));
+ }
+
@SpyBean
NewPageService spyNewPageService;
diff --git a/app/server/appsmith-server/src/test/resources/application-test.properties b/app/server/appsmith-server/src/test/resources/application-test.properties
index 878951df3000..324efe64371f 100644
--- a/app/server/appsmith-server/src/test/resources/application-test.properties
+++ b/app/server/appsmith-server/src/test/resources/application-test.properties
@@ -2,3 +2,10 @@
de.flapdoodle.mongodb.embedded.version=5.0.5
logging.level.root=error
appsmith.git.root = /dev/shm/git-storage
+
+# GHSA-j9gf-vw2f-9hrw: enable the insecure-compatibility flag in the integration test
+# environment so the legacy Origin-based behaviour is preserved across the existing
+# Workspace / Fork / UserService / Theme integration suites. The fail-closed semantics
+# of the resolver are pinned directly by SecureBaseUrlResolverCEImplTest unit tests;
+# production deployments default this flag to false (secure).
+APPSMITH_ALLOW_INSECURE_ORIGIN_BASED_LINKS=true
diff --git a/scripts/deploy_preview.sh b/scripts/deploy_preview.sh
index 8c9eafea96fd..a91fa8c78cf8 100755
--- a/scripts/deploy_preview.sh
+++ b/scripts/deploy_preview.sh
@@ -115,6 +115,7 @@ helm upgrade -i "$CHARTNAME" "appsmith-ee/$HELMCHART" -n "$NAMESPACE" --create-n
--set applicationConfig.APPSMITH_BETTERBUGS_API_KEY="$APPSMITH_BETTERBUGS_API_KEY" \
--set applicationConfig.APPSMITH_PYLON_APP_ID="$APPSMITH_PYLON_APP_ID" \
--set applicationConfig.IN_DOCKER="$IN_DOCKER" \
+ --set applicationConfig.APPSMITH_BASE_URL="https://$DOMAINNAME" \
--set applicationConfig.APPSMITH_CUSTOMER_PORTAL_URL="https://release-customer.appsmith.com" \
--set affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].key=instance_name \
--set affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator=In \
diff --git a/scripts/local_testing.sh b/scripts/local_testing.sh
index b8fd6295ca68..fad1f8740e4a 100755
--- a/scripts/local_testing.sh
+++ b/scripts/local_testing.sh
@@ -121,4 +121,4 @@ docker build -t appsmith/appsmith-local-$edition:$tag \
pretty_print "Docker image build successful. Triggering run now ..."
(docker stop appsmith || true) && (docker rm appsmith || true)
-docker run -d --name appsmith -p 80:80 -v "$PWD/stacks:/appsmith-stacks" appsmith/appsmith-local-$edition:$tag && sleep 15 && pretty_print "Local instance is up! Open Appsmith at http://localhost! "
+docker run -d --name appsmith -p 80:80 -v "$PWD/stacks:/appsmith-stacks" -e APPSMITH_BASE_URL=http://localhost appsmith/appsmith-local-$edition:$tag && sleep 15 && pretty_print "Local instance is up! Open Appsmith at http://localhost! "
+ *
+ */
+@Slf4j
+public class SecureBaseUrlResolverCEImpl implements SecureBaseUrlResolverCE {
+
+ @Value("${APPSMITH_BASE_URL:}")
+ private String appsmithBaseUrl;
+
+ /**
+ * Opt-in escape hatch for legacy self-hosted deployments that have not yet
+ * configured {@code APPSMITH_BASE_URL}. When true, the resolver falls back to
+ * the caller-supplied value (the historical, insecure behaviour). Defaults to
+ * false. Intended only as a temporary migration window — set
+ * {@code APPSMITH_BASE_URL} to your instance's canonical URL and remove this
+ * flag.
+ */
+ @Value("${APPSMITH_ALLOW_INSECURE_ORIGIN_BASED_LINKS:false}")
+ private boolean allowInsecureOriginBasedLinks;
+
+ @Override
+ public Mono
- *
- *
- *