Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a18d1d7
test(security): add failing tests for SecureBaseUrlResolver (GHSA-j9g…
subrata71 Apr 27, 2026
3b7c865
fix(security): fail closed when APPSMITH_BASE_URL unset for token-bea…
subrata71 Apr 27, 2026
35eb077
fix(security): unblock CI for GHSA-j9gf-vw2f-9hrw — unique error code…
subrata71 Apr 28, 2026
dd8fbed
ci: configure APPSMITH_BASE_URL for E2E test environments (GHSA-j9gf-…
subrata71 Apr 28, 2026
f020589
fix(security): compare APPSMITH_BASE_URL and Origin by URL origin (GH…
subrata71 Apr 28, 2026
67b27ca
test(cypress): use real baseUrl as Origin in admin/invite intercepts …
subrata71 Apr 29, 2026
306e492
fix(security): bump MISCONFIGURED_INSTANCE_BASE_URL to AE-APP-5048 (G…
subrata71 Apr 29, 2026
acdb86c
docs(security): design — admin warning banner for unset APPSMITH_BASE…
subrata71 Apr 29, 2026
34c695b
docs(security): implementation plan — admin base-url warning banner (…
subrata71 Apr 29, 2026
57cf9ae
feat(security): add isBaseUrlConfigurationHealthy() to SecureBaseUrlR…
subrata71 Apr 29, 2026
9cd2226
feat(security): add instanceBaseUrlConfigurationHealthy field to User…
subrata71 Apr 29, 2026
afce357
feat(security): wire SecureBaseUrlResolver into UserServiceCEImpl#bui…
subrata71 Apr 29, 2026
2881a8e
feat(client): add base-url banner copy + selector + User type field (…
subrata71 Apr 29, 2026
58e8705
feat(client): add BaseUrlMissingBanner component (GHSA-j9gf-vw2f-9hrw)
subrata71 Apr 29, 2026
46f8118
feat(client): mount BaseUrlMissingBanner above AppHeader in CE AppRou…
subrata71 Apr 29, 2026
6bfcea7
refactor(security): move instanceBaseUrlConfigurationHealthy from Use…
subrata71 Apr 30, 2026
982c4fc
fix(security): break EE construction-time cycle on SecureBaseUrlResol…
subrata71 Apr 30, 2026
b6bbdcc
refactor(security): break circular dep by relocating resolver call to…
subrata71 Apr 30, 2026
d3fa256
test(security): mock SecureBaseUrlResolver in ConsolidatedAPIServiceI…
subrata71 Apr 30, 2026
a5b32ff
fix(client): rebuild BaseUrlMissingBanner using ADS Banner pattern (G…
subrata71 Apr 30, 2026
5e8ad49
fix(client): mount BaseUrlMissingBanner inside PageHeader to fix z-in…
subrata71 Apr 30, 2026
e9e92ce
chore: remove superpowers planning docs from PR
subrata71 May 5, 2026
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 .github/workflows/ci-test-custom-script.yml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ jobs:
-e APPSMITH_CLOUD_SERVICES_BASE_URL=http://host.docker.internal:5001 \
-e APPSMITH_CLOUD_SERVICES_SIGNATURE_BASE_URL=http://host.docker.internal:8090 \
-e APPSMITH_RATE_LIMIT=1000 \
-e APPSMITH_BASE_URL=http://localhost \
--add-host=host.docker.internal:host-gateway --add-host=api.segment.io:host-gateway --add-host=t.appsmith.com:host-gateway \
cicontainer

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci-test-limited-with-count.yml
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ jobs:
-e APPSMITH_DISABLE_TELEMETRY=true \
-e APPSMITH_INTERCOM_APP_ID=DUMMY_VALUE \
-e APPSMITH_CLOUD_SERVICES_BASE_URL=http://host.docker.internal:5001 \
-e APPSMITH_BASE_URL=http://localhost \
--add-host=host.docker.internal:host-gateway --add-host=api.segment.io:host-gateway --add-host=t.appsmith.com:host-gateway \
cicontainer
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci-test-limited.yml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ jobs:
-e APPSMITH_DISABLE_TELEMETRY=true \
-e APPSMITH_INTERCOM_APP_ID=DUMMY_VALUE \
-e APPSMITH_CLOUD_SERVICES_BASE_URL=http://host.docker.internal:5001 \
-e APPSMITH_BASE_URL=http://localhost \
--add-host=host.docker.internal:host-gateway --add-host=api.segment.io:host-gateway --add-host=t.appsmith.com:host-gateway \
cicontainer

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci-test-playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ jobs:
-e APPSMITH_CLOUD_SERVICES_BASE_URL=http://host.docker.internal:5001 \
-e APPSMITH_CLOUD_SERVICES_SIGNATURE_BASE_URL=http://host.docker.internal:8090 \
-e APPSMITH_RATE_LIMIT=1000 \
-e APPSMITH_BASE_URL=http://localhost \
-e APPSMITH_CARBON_API_BASE_PATH=https://carbon.appsmith.com \
-e APPSMITH_CARBON_API_KEY="$APPSMITH_CARBON_API_KEY" \
-e APPSMITH_AI_SERVER_MANAGED_HOSTING=true \
Expand Down
19 changes: 14 additions & 5 deletions app/client/cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,15 @@ export const addIndexedDBKey = (key, value) => {
};

Cypress.Commands.add("stubPostHeaderReq", () => {
// GHSA-j9gf-vw2f-9hrw: server-side strict-mode now requires Origin to match
// APPSMITH_BASE_URL. Use the live Cypress baseUrl so the intercept produces a
// valid Origin (the literal string "Cypress" used to live here as a synthetic-
// traffic flag and no longer parses as a URL).
cy.intercept("POST", "/api/v1/users/invite", (req) => {
req.headers["origin"] = "Cypress";
req.headers["origin"] = Cypress.config("baseUrl");
}).as("mockPostInvite");
cy.intercept("POST", "/api/v1/applications/invite", (req) => {
req.headers["origin"] = "Cypress";
req.headers["origin"] = Cypress.config("baseUrl");
}).as("mockPostAppInvite");
});

Expand Down Expand Up @@ -752,7 +756,8 @@ Cypress.Commands.add("startServerAndRoutes", () => {
hostname: window.location.host,
},
(req) => {
req.headers["origin"] = "Cypress";
// GHSA-j9gf-vw2f-9hrw: send a real Origin matching APPSMITH_BASE_URL.
req.headers["origin"] = Cypress.config("baseUrl");
},
).as("connectGitLocalRepo");

Expand All @@ -766,13 +771,17 @@ Cypress.Commands.add("startServerAndRoutes", () => {
});

cy.intercept("PUT", "/api/v1/admin/env", (req) => {
req.headers["origin"] = "Cypress";
// GHSA-j9gf-vw2f-9hrw: server-side strict-mode now requires Origin to match
// APPSMITH_BASE_URL. Use the live Cypress baseUrl instead of the legacy
// synthetic-traffic flag "Cypress" (which no longer parses as a URL).
req.headers["origin"] = Cypress.config("baseUrl");
}).as("postEnv");

cy.intercept("GET", "/settings/general").as("getGeneral");
cy.intercept("GET", "/api/v1/tenants/current").as("signUpLogin");
cy.intercept("PUT", "/api/v1/tenants", (req) => {
req.headers["origin"] = "Cypress";
// GHSA-j9gf-vw2f-9hrw: see note above.
req.headers["origin"] = Cypress.config("baseUrl");
}).as("postTenant");
cy.intercept("PUT", "/api/v1/git/applications/*/discard").as(
"discardChanges",
Expand Down
6 changes: 6 additions & 0 deletions app/client/src/ce/constants/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,12 @@ export const READ_DOCUMENTATION = () => "Read documentation";
export const LEARN_MORE = () => "Learn more";

export const I_UNDERSTAND = () => "I understand";

// Admin warning banner — shown when APPSMITH_BASE_URL is unset and the resolver is in
// fail-closed mode for token-bearing email flows. See GHSA-j9gf-vw2f-9hrw.
export const BASE_URL_MISSING_BANNER_BODY = () =>
"Email delivery is disabled — forgot-password, email-verification and invite emails will not be sent until APPSMITH_BASE_URL is configured.";
export const BASE_URL_MISSING_BANNER_CTA = () => "Configure APPSMITH_BASE_URL";
export const GIT_NO_UPDATED_TOOLTIP = () => "No new updates to push";

export const FIND_OR_CREATE_A_BRANCH = () => "Find or create a branch";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import { Provider } from "react-redux";
import configureStore from "redux-mock-store";
import { ThemeProvider } from "styled-components";
import { BrowserRouter as Router } from "react-router-dom";
import { lightTheme } from "selectors/themeSelectors";
import BaseUrlMissingBanner from "./BaseUrlMissingBanner";

const mockStore = configureStore([]);

const storeWith = (currentUser: object | null, orgConfig?: object) =>
mockStore({
ui: { users: { currentUser } },
organization: orgConfig ? { organizationConfiguration: orgConfig } : {},
});

const renderBanner = (currentUser: object | null, orgConfig?: object) =>
render(
<Provider store={storeWith(currentUser, orgConfig)}>
<ThemeProvider theme={lightTheme}>
<Router>
<BaseUrlMissingBanner />
</Router>
</ThemeProvider>
</Provider>,
);

const SUPER_ADMIN = {
isSuperUser: true,
adminSettingsVisible: true,
};

describe("BaseUrlMissingBanner — GHSA-j9gf-vw2f-9hrw", () => {
it("renders for super-user with admin settings visible and unhealthy org config", () => {
renderBanner(SUPER_ADMIN, { instanceBaseUrlConfigurationHealthy: false });

expect(
screen.getByTestId("t--base-url-missing-banner"),
).toBeInTheDocument();
expect(screen.getByText(/Email delivery is disabled/i)).toBeInTheDocument();
});

it("does not render when org config is healthy", () => {
const { container } = renderBanner(SUPER_ADMIN, {
instanceBaseUrlConfigurationHealthy: true,
});

expect(container.firstChild).toBeNull();
});

it("does not render for non-super-user", () => {
const { container } = renderBanner(
{ isSuperUser: false, adminSettingsVisible: true },
{ instanceBaseUrlConfigurationHealthy: false },
);

expect(container.firstChild).toBeNull();
});

it("does not render when admin settings hidden (RBAC / license guard)", () => {
const { container } = renderBanner(
{ isSuperUser: true, adminSettingsVisible: false },
{ instanceBaseUrlConfigurationHealthy: false },
);

expect(container.firstChild).toBeNull();
});

// Rolling-deploy safety: org config without the new field. Banner must stay
// hidden, not flip on as a false positive.
it("does not render when health field is missing from org config (rolling deploy)", () => {
const { container } = renderBanner(SUPER_ADMIN, {});

expect(container.firstChild).toBeNull();
});

it("does not render when org config is missing entirely", () => {
const { container } = renderBanner(SUPER_ADMIN);

expect(container.firstChild).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from "react";
import { useSelector } from "react-redux";
import styled from "styled-components";
import { Banner } from "@appsmith/ads";
import { getShouldShowBaseUrlMissingBanner } from "selectors/usersSelectors";
import { adminSettingsCategoryUrl } from "ee/RouteBuilder";
import { SettingCategories } from "ee/pages/AdminSettings/config/types";
import {
BASE_URL_MISSING_BANNER_BODY,
BASE_URL_MISSING_BANNER_CTA,
createMessage,
} from "ee/constants/messages";

/**
* GHSA-j9gf-vw2f-9hrw — admin warning banner shown to instance super-users when
* the server's SecureBaseUrlResolver reports that APPSMITH_BASE_URL is unset and
* token-bearing email flows are therefore disabled.
*
* Uses the same `Banner` ADS component + `position: fixed; top: 0` styling as
* the existing PageBannerMessage (license/trial banner) — single source of truth
* for top-of-screen banners across the product. Not user-dismissible: banner
* reflects live server state and clears on the next bootstrap fetch after the
* admin sets APPSMITH_BASE_URL via Admin Settings (which triggers the existing
* Configuration-tier server-restart + SPA-reload flow).
*/
const StyledBanner = styled(Banner)`
position: fixed;
z-index: 2;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
top: 0;
`;

const BaseUrlMissingBanner: React.FC = () => {
const shouldShow = useSelector(getShouldShowBaseUrlMissingBanner);

if (!shouldShow) return null;

return (
<StyledBanner
data-testid="t--base-url-missing-banner"
kind="warning"
link={{
children: createMessage(BASE_URL_MISSING_BANNER_CTA),
to: adminSettingsCategoryUrl({
category: SettingCategories.CONFIGURATION,
}),
}}
>
{createMessage(BASE_URL_MISSING_BANNER_BODY)}
</StyledBanner>
);
};

export default BaseUrlMissingBanner;
17 changes: 15 additions & 2 deletions app/client/src/pages/common/PageHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React, { useEffect } from "react";
import { useRouteMatch } from "react-router-dom";
import { connect, useDispatch, useSelector } from "react-redux";
import { getCurrentUser } from "selectors/usersSelectors";
import {
getCurrentUser,
getShouldShowBaseUrlMissingBanner,
} from "selectors/usersSelectors";
import styled from "styled-components";
import StyledHeader from "components/designSystems/appsmith/StyledHeader";
import type { DefaultRootState } from "react-redux";
Expand All @@ -10,6 +13,7 @@ import { useIsMobileDevice } from "utils/hooks/useDeviceDetect";
import { getTemplateNotificationSeenAction } from "actions/templateActions";
import { shouldShowLicenseBanner } from "ee/selectors/organizationSelectors";
import { Banner } from "ee/utils/licenseHelpers";
import BaseUrlMissingBanner from "components/editorComponents/BaseUrlMissingBanner";
import bootIntercom from "utils/bootIntercom";
import EntitySearchBar from "pages/common/SearchBar/EntitySearchBar";

Expand Down Expand Up @@ -61,14 +65,23 @@ export function PageHeader(props: PageHeaderProps) {
const showBanner = useSelector(shouldShowLicenseBanner);
const isHomePage = useRouteMatch("/applications")?.isExact;
const isLicensePage = useRouteMatch("/license")?.isExact;
// GHSA-j9gf-vw2f-9hrw — fold the base-url-missing admin banner into the same
// isBannerVisible signal that the existing license/trial banner uses, so the
// page header gets pushed down by the banner's height (40px desktop / 70px
// mobile) when either banner is visible. Without this, the page header — which
// has a higher z-index — paints over the fixed-position banner at top: 0.
const showBaseUrlBanner = useSelector(getShouldShowBaseUrlMissingBanner);
const isAnyBannerVisible =
(showBanner && (isHomePage || isLicensePage)) || showBaseUrlBanner;

return (
<>
<Banner />
<BaseUrlMissingBanner />
<StyledPageHeader
data-testid="t--appsmith-page-header"
hideShadow={props.hideShadow || false}
isBannerVisible={showBanner && (isHomePage || isLicensePage)}
isBannerVisible={isAnyBannerVisible}
isMobile={isMobile}
showSeparator={props.showSeparator || false}
>
Expand Down
82 changes: 82 additions & 0 deletions app/client/src/selectors/usersSelectors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { getShouldShowBaseUrlMissingBanner } from "./usersSelectors";

// Minimal Redux state shape; the selector reads ui.users.currentUser AND
// organization.organizationConfiguration.
const stateWith = (
currentUser: object | null,
orgConfig: object | undefined = undefined,
) =>
({
ui: { users: { currentUser } },
organization: orgConfig
? { organizationConfiguration: orgConfig }
: undefined,
}) as never;

const SUPER_ADMIN = {
isSuperUser: true,
adminSettingsVisible: true,
};

describe("getShouldShowBaseUrlMissingBanner — GHSA-j9gf-vw2f-9hrw", () => {
it("returns true for super user with admin settings visible and unhealthy org config", () => {
expect(
getShouldShowBaseUrlMissingBanner(
stateWith(SUPER_ADMIN, { instanceBaseUrlConfigurationHealthy: false }),
),
).toBe(true);
});

it("returns false when org config is healthy", () => {
expect(
getShouldShowBaseUrlMissingBanner(
stateWith(SUPER_ADMIN, { instanceBaseUrlConfigurationHealthy: true }),
),
).toBe(false);
});

it("returns false for non-super-user", () => {
expect(
getShouldShowBaseUrlMissingBanner(
stateWith(
{ isSuperUser: false, adminSettingsVisible: true },
{ instanceBaseUrlConfigurationHealthy: false },
),
),
).toBe(false);
});

it("returns false when admin settings hidden (RBAC / license guard)", () => {
expect(
getShouldShowBaseUrlMissingBanner(
stateWith(
{ isSuperUser: true, adminSettingsVisible: false },
{ instanceBaseUrlConfigurationHealthy: false },
),
),
).toBe(false);
});

// Rolling-deploy safety: newer client briefly paired with older server has the
// health field absent on org config. Banner must stay hidden, not flip on as a
// false positive.
it("returns false when health field is missing from org config (rolling deploy)", () => {
expect(getShouldShowBaseUrlMissingBanner(stateWith(SUPER_ADMIN, {}))).toBe(
false,
);
});

it("returns false when org config is missing entirely", () => {
expect(getShouldShowBaseUrlMissingBanner(stateWith(SUPER_ADMIN))).toBe(
false,
);
});

it("returns false when currentUser is null", () => {
expect(
getShouldShowBaseUrlMissingBanner(
stateWith(null, { instanceBaseUrlConfigurationHealthy: false }),
),
).toBe(false);
});
});
31 changes: 31 additions & 0 deletions app/client/src/selectors/usersSelectors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,34 @@ export const getFeatureFlagsFetching = (state: DefaultRootState) =>

export const getIsUserLoggedIn = (state: DefaultRootState): boolean =>
state.ui.users.currentUser?.email !== ANONYMOUS_USERNAME;

/**
* GHSA-j9gf-vw2f-9hrw — admin warning banner gate. Returns true only when ALL of:
* - the current user is an instance super user (user-level gate),
* - admin settings are visible to them (RBAC / license tier guard),
* - and the server explicitly reports the org config's
* `instanceBaseUrlConfigurationHealthy === false` (instance-level signal).
*
* The instance signal lives on `state.organization.organizationConfiguration`
* (populated from the always-called `/v1/consolidated-api` bootstrap), not on
* `currentUser`. `/v1/users/me` is rarely re-fetched on dashboard routes — the
* canonical bootstrap is consolidated-api, and org config is the right bucket
* within it for instance-state.
*
* The explicit `=== false` (rather than `!value`) is deliberate: during a rolling
* deploy where a newer client briefly sees an older server's response without the
* field, `undefined === false` is `false`, so the banner stays hidden until both
* sides are deployed.
*/
export const getShouldShowBaseUrlMissingBanner = (
state: DefaultRootState,
): boolean => {
const user = state.ui?.users?.currentUser;
const orgConfig = state.organization?.organizationConfiguration;

return Boolean(
user?.isSuperUser &&
user?.adminSettingsVisible &&
orgConfig?.instanceBaseUrlConfigurationHealthy === false,
);
};
Loading
Loading