From 04248e2a92f29e12d7dc843cb669fc8689956e48 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Mon, 25 May 2026 10:57:02 -0700 Subject: [PATCH 1/2] Safe redirect action --- core/src/org/labkey/core/CoreController.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/core/src/org/labkey/core/CoreController.java b/core/src/org/labkey/core/CoreController.java index acaf441e3aa..3bee1152269 100644 --- a/core/src/org/labkey/core/CoreController.java +++ b/core/src/org/labkey/core/CoreController.java @@ -35,7 +35,9 @@ import org.labkey.api.action.ExportAction; import org.labkey.api.action.MutatingApiAction; import org.labkey.api.action.ReadOnlyApiAction; +import org.labkey.api.action.ReturnUrlForm; import org.labkey.api.action.SimpleApiJsonForm; +import org.labkey.api.action.SimpleRedirectAction; import org.labkey.api.action.SimpleViewAction; import org.labkey.api.action.SpringActionController; import org.labkey.api.admin.AbstractFolderContext.ExportType; @@ -204,10 +206,6 @@ import static org.labkey.api.view.template.WarningService.SESSION_WARNINGS_BANNER_KEY; -/** - * User: jeckels - * Date: Jan 4, 2007 - */ public class CoreController extends SpringActionController { private static final Map _customStylesheetCache = new ConcurrentHashMap<>(); @@ -2908,4 +2906,16 @@ public void setProvider(String provider) } + // Called by various client components to ensure safe redirects, GitHub Issue #1023. This action redirects to + // local URLs only, never to an external site, even if the host is on the "Allowed External Redirect Hosts" list. + @SuppressWarnings("unused") + @RequiresNoPermission + public static class SafeRedirectAction extends SimpleRedirectAction + { + @Override + public ActionURL getRedirectURL(ReturnUrlForm form) throws Exception + { + return form.getReturnActionURL(AppProps.getInstance().getHomePageActionURL()); + } + } } From 43b7ae8c2e170ad17403d9119b56310e27430f9e Mon Sep 17 00:00:00 2001 From: cnathe Date: Wed, 27 May 2026 16:44:21 -0500 Subject: [PATCH 2/2] Update window.location.href returnUrl usages to call core-safeRedirect instead - note: I added a TODO for refactoring this but that should wait until we merge to develop since it will save us from having to bump the package version in 26.3 for this --- core/src/client/AssayDesigner/AssayDesigner.tsx | 5 +++-- core/src/client/DataClassDesigner/DataClassDesigner.tsx | 5 ++--- core/src/client/DatasetDesigner/DatasetDesigner.tsx | 5 ++--- core/src/client/DomainDesigner/DomainDesigner.tsx | 5 ++--- core/src/client/IssuesListDesigner/IssuesListDesigner.tsx | 5 ++--- core/src/client/ListDesigner/ListDesigner.tsx | 4 ++-- core/src/client/SampleTypeDesigner/SampleTypeDesigner.tsx | 5 ++--- 7 files changed, 15 insertions(+), 19 deletions(-) diff --git a/core/src/client/AssayDesigner/AssayDesigner.tsx b/core/src/client/AssayDesigner/AssayDesigner.tsx index 518b97ca595..3634428ec7a 100644 --- a/core/src/client/AssayDesigner/AssayDesigner.tsx +++ b/core/src/client/AssayDesigner/AssayDesigner.tsx @@ -140,8 +140,9 @@ export class App extends React.Component { navigate(defaultUrl: string) { this._dirty = false; - - window.location.href = this.state.returnUrl || defaultUrl; + const redirectUrl = this.state.returnUrl || defaultUrl; + // TODO refactor this and other usages in platform/core/src/client to a helper safeRedirect() function from @labkey/components + window.location.href = ActionURL.buildURL('core', 'safeRedirect', undefined, { returnUrl: redirectUrl }); } onCancel = (): void => { diff --git a/core/src/client/DataClassDesigner/DataClassDesigner.tsx b/core/src/client/DataClassDesigner/DataClassDesigner.tsx index 4a19ca02075..422d3aac188 100644 --- a/core/src/client/DataClassDesigner/DataClassDesigner.tsx +++ b/core/src/client/DataClassDesigner/DataClassDesigner.tsx @@ -68,9 +68,8 @@ class DataClassDesignerWrapper extends React.Component { navigate(defaultUrl: string) { this._dirty = false; - - const returnUrl = ActionURL.getReturnUrl(); - window.location.href = returnUrl || defaultUrl; + const redirectUrl = ActionURL.getReturnUrl() || defaultUrl; + window.location.href = ActionURL.buildURL('core', 'safeRedirect', undefined, { returnUrl: redirectUrl }); } onCancel = () => { diff --git a/core/src/client/DatasetDesigner/DatasetDesigner.tsx b/core/src/client/DatasetDesigner/DatasetDesigner.tsx index 50eec7df716..f30c22ebd52 100644 --- a/core/src/client/DatasetDesigner/DatasetDesigner.tsx +++ b/core/src/client/DatasetDesigner/DatasetDesigner.tsx @@ -66,9 +66,8 @@ class DatasetDesigner extends PureComponent { navigate(defaultUrl: string): void { this._dirty = false; - - const returnUrl = ActionURL.getReturnUrl(); - window.location.href = returnUrl || defaultUrl; + const redirectUrl = ActionURL.getReturnUrl() || defaultUrl; + window.location.href = ActionURL.buildURL('core', 'safeRedirect', undefined, { returnUrl: redirectUrl }); } navigateOnComplete(model: DatasetModel): void { diff --git a/core/src/client/DomainDesigner/DomainDesigner.tsx b/core/src/client/DomainDesigner/DomainDesigner.tsx index 08ae19c2d91..240bf106d04 100644 --- a/core/src/client/DomainDesigner/DomainDesigner.tsx +++ b/core/src/client/DomainDesigner/DomainDesigner.tsx @@ -161,9 +161,8 @@ class DomainDesigner extends React.PureComponent> { navigate = (): void => { this._dirty = false; - - const returnUrl = ActionURL.getReturnUrl(); - window.location.href = returnUrl || ActionURL.buildURL('project', 'begin', getServerContext().container.path); + const redirectUrl = ActionURL.getReturnUrl() || ActionURL.buildURL('project', 'begin', getServerContext().container.path); + window.location.href = ActionURL.buildURL('core', 'safeRedirect', undefined, { returnUrl: redirectUrl }); }; renderWarningConfirm() { diff --git a/core/src/client/IssuesListDesigner/IssuesListDesigner.tsx b/core/src/client/IssuesListDesigner/IssuesListDesigner.tsx index da294bfad17..d9dd54ba085 100644 --- a/core/src/client/IssuesListDesigner/IssuesListDesigner.tsx +++ b/core/src/client/IssuesListDesigner/IssuesListDesigner.tsx @@ -81,9 +81,8 @@ class IssuesListDesigner extends React.Component<{}, State> { navigate = (defaultUrl: string) => { this._dirty = false; - - const returnUrl = ActionURL.getReturnUrl(); - window.location.href = returnUrl || defaultUrl; + const redirectUrl = ActionURL.getReturnUrl() || defaultUrl; + window.location.href = ActionURL.buildURL('core', 'safeRedirect', undefined, { returnUrl: redirectUrl }); }; onComplete = (model: IssuesListDefModel) => { diff --git a/core/src/client/ListDesigner/ListDesigner.tsx b/core/src/client/ListDesigner/ListDesigner.tsx index f3c0ad5da80..9f5022b3788 100644 --- a/core/src/client/ListDesigner/ListDesigner.tsx +++ b/core/src/client/ListDesigner/ListDesigner.tsx @@ -104,8 +104,8 @@ export class ListDesigner extends React.Component { navigate = async (returnUrlProvider: () => Promise, model?: ListModel): Promise => { this._dirty = false; - - window.location.href = this.getReturnUrl(model) ?? (await returnUrlProvider()); + const redirectUrl = this.getReturnUrl(model) ?? (await returnUrlProvider()); + window.location.href = ActionURL.buildURL('core', 'safeRedirect', undefined, { returnUrl: redirectUrl }); }; getReturnUrl = (model?: ListModel): string => { diff --git a/core/src/client/SampleTypeDesigner/SampleTypeDesigner.tsx b/core/src/client/SampleTypeDesigner/SampleTypeDesigner.tsx index e3b0e084b06..cb68d02ba79 100644 --- a/core/src/client/SampleTypeDesigner/SampleTypeDesigner.tsx +++ b/core/src/client/SampleTypeDesigner/SampleTypeDesigner.tsx @@ -137,9 +137,8 @@ class SampleTypeDesignerWrapper extends React.PureComponent { navigate(defaultUrl: string) { this._dirty = false; - - const returnUrl = ActionURL.getReturnUrl(); - window.location.href = returnUrl || defaultUrl; + const redirectUrl = ActionURL.getReturnUrl() || defaultUrl; + window.location.href = ActionURL.buildURL('core', 'safeRedirect', undefined, { returnUrl: redirectUrl }); } render() {