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
5 changes: 3 additions & 2 deletions core/src/client/AssayDesigner/AssayDesigner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,9 @@ export class App extends React.Component<any, State> {

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as noted in the commit message, I'll wait until this gets merged to develop to address this TODO so that I don't need to bump @labkey/components for this branch (and have to handle the merge forward conflicts)

window.location.href = ActionURL.buildURL('core', 'safeRedirect', undefined, { returnUrl: redirectUrl });
}

onCancel = (): void => {
Expand Down
5 changes: 2 additions & 3 deletions core/src/client/DataClassDesigner/DataClassDesigner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,8 @@ class DataClassDesignerWrapper extends React.Component<any, 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 });
}

onCancel = () => {
Expand Down
5 changes: 2 additions & 3 deletions core/src/client/DatasetDesigner/DatasetDesigner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,8 @@ class DatasetDesigner extends PureComponent<any, State> {

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 {
Expand Down
5 changes: 2 additions & 3 deletions core/src/client/DomainDesigner/DomainDesigner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,8 @@ class DomainDesigner extends React.PureComponent<any, Partial<IAppState>> {

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() {
Expand Down
5 changes: 2 additions & 3 deletions core/src/client/IssuesListDesigner/IssuesListDesigner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
4 changes: 2 additions & 2 deletions core/src/client/ListDesigner/ListDesigner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ export class ListDesigner extends React.Component<Props, State> {

navigate = async (returnUrlProvider: () => Promise<string>, model?: ListModel): Promise<void> => {
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 => {
Expand Down
5 changes: 2 additions & 3 deletions core/src/client/SampleTypeDesigner/SampleTypeDesigner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,8 @@ class SampleTypeDesignerWrapper extends React.PureComponent<any, 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 });
}

render() {
Expand Down
22 changes: 18 additions & 4 deletions core/src/org/labkey/core/CoreController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Container, Content> _customStylesheetCache = new ConcurrentHashMap<>();
Expand Down Expand Up @@ -2908,4 +2906,20 @@ 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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is it that assures this uses local URLs only? Doesn't seem to be codified here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, ActionURL is guaranteed to be a local URL. You can give it an external URL and it'll ignore the schema, host, and port, always using the local AppProps values (e.g., when calling getURIString()). Second, SimpleRedirectAction throws RedirectException, which now guarantees a local redirect. This is the result of a recent refactor I did to tighten up our server-side redirect handling. The only way to redirect externally now is via ExternalRedirectException (which ensures an allowed host) or UnsafeExternalRedirectException (where caller is responsible for ensuring the URL is safe).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, Adam!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: I expanded the action's comment to lay out the safety guarantees similar to the above

// Why is this safe? First, ActionURL is guaranteed to be a local URL (schema, host, and port are always taken
// from local AppProps, even if an absolute URL is requested via getURIString() or similar). Second,
// SimpleRedirectAction throws RedirectException which also guarantees local redirects (instances of that class
// always use getLocalURIString()).
@SuppressWarnings("unused")
@RequiresNoPermission
public static class SafeRedirectAction extends SimpleRedirectAction<ReturnUrlForm>
{
@Override
public ActionURL getRedirectURL(ReturnUrlForm form) throws Exception
{
return form.getReturnActionURL(AppProps.getInstance().getHomePageActionURL());
}
}
}
26 changes: 25 additions & 1 deletion study/test/src/org/labkey/test/tests/study/AssayTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.labkey.test.categories.Assays;
import org.labkey.test.categories.Daily;
import org.labkey.test.components.CustomizeView;
import org.labkey.test.components.DomainDesignerPage;
import org.labkey.test.components.assay.AssayConstants;
import org.labkey.test.components.domain.DomainFieldRow;
import org.labkey.test.components.domain.DomainFormPanel;
Expand All @@ -48,13 +49,13 @@
import org.labkey.test.util.TestDataGenerator;
import org.labkey.test.util.data.TestDataUtils;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

Expand All @@ -69,6 +70,7 @@ public class AssayTest extends AbstractAssayTest
private static final String ISSUE_53616_ASSAY = "Issue53616Assay";
private static final String ISSUE_53616_PROJECT = "Issue53616Project" + TRICKY_CHARACTERS_FOR_PROJECT_NAMES;
private static final String ISSUE_53831_PROJECT = "Issue53831Project" + TRICKY_CHARACTERS_FOR_PROJECT_NAMES;
private static final String GH_1023_PROJECT = "GH1023Project" + TRICKY_CHARACTERS_FOR_PROJECT_NAMES;
private static final String SAMPLE_FIELD_TEST_ASSAY = "SampleFieldTestAssay";
private static final String SAMPLE_FIELD_PROJECT_NAME = "Sample Field Test Project" + TRICKY_CHARACTERS_FOR_PROJECT_NAMES;

Expand All @@ -90,6 +92,7 @@ protected void doCleanup(boolean afterTest) throws TestTimeoutException
_containerHelper.deleteProject(ISSUE_53616_PROJECT, false);
_containerHelper.deleteProject(ISSUE_53625_PROJECT, false);
_containerHelper.deleteProject(ISSUE_53831_PROJECT, false);
_containerHelper.deleteProject(GH_1023_PROJECT, false);

_userHelper.deleteUsers(false, TEST_ASSAY_USR_PI1, TEST_ASSAY_USR_TECH1);
}
Expand Down Expand Up @@ -1142,6 +1145,27 @@ private void verifyAssayImportForLookupValidator(String assayName, FieldInfo loo
checker().verifyEquals("Lookup values not as expected.", List.of("123"), dataTable.getColumnDataAsText(lookupField.getLabel()));
}

@Test // GitHub Issue #1023
public void testNoExternalReturnUrlRedirect() throws Exception
{
_containerHelper.createProject(GH_1023_PROJECT, "Assay");
goToProjectHome(GH_1023_PROJECT);

// Navigate to domain designer with an external returnUrl. The safeRedirect action
// should prevent external redirects, falling back to the local home page instead.
String domainDesignerUrl = WebTestHelper.buildURL("assay", GH_1023_PROJECT, "designer",
Map.of("providerName", "General", "returnUrl", "https://labkey.com"));
beginAt(domainDesignerUrl);
DomainDesignerPage domainDesignerPage = new DomainDesignerPage(getDriver());
domainDesignerPage.fieldsPanel();
domainDesignerPage.clickCancel();
String postCancelUrl = getDriver().getCurrentUrl();
assertFalse("Cancel with an external returnUrl should not navigate to an external site",
postCancelUrl.contains("labkey.com"));
assertTrue("Cancel with an external returnUrl should redirect to a local LabKey page instead of: " + postCancelUrl,
WebTestHelper.isTestServerUrl(postCancelUrl));
}

@Override
protected BrowserType bestBrowser()
{
Expand Down
19 changes: 19 additions & 0 deletions study/test/src/org/labkey/test/tests/study/StudyDatasetsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.labkey.test.TestFileUtils;
import org.labkey.test.WebTestHelper;
import org.labkey.test.categories.Daily;
import org.labkey.test.components.DomainDesignerPage;
import org.labkey.test.components.LookAndFeelScatterPlot;
import org.labkey.test.components.LookAndFeelTimeChart;
import org.labkey.test.components.domain.DomainFormPanel;
Expand Down Expand Up @@ -784,4 +785,22 @@ protected void clickViewDetailsLink(String reportName)
waitForElement(link);
clickAndWait(link);
}

@Test // GitHub Issue #1023
public void testNoExternalReturnUrlRedirect() throws Exception
{
// Navigate to domain designer with an external returnUrl. The safeRedirect action
// should prevent external redirects, falling back to the local home page instead.
String domainDesignerUrl = WebTestHelper.buildURL("study", getProjectName() + "/" + getFolderName(), "defineDatasetType",
Map.of("returnUrl", "https://labkey.com"));
beginAt(domainDesignerUrl);
DomainDesignerPage domainDesignerPage = new DomainDesignerPage(getDriver());
domainDesignerPage.fieldsPanel();
domainDesignerPage.clickCancel();
String postCancelUrl = getDriver().getCurrentUrl();
assertFalse("Cancel with an external returnUrl should not navigate to an external site",
postCancelUrl.contains("labkey.com"));
assertTrue("Cancel with an external returnUrl should redirect to a local LabKey page instead of: " + postCancelUrl,
WebTestHelper.isTestServerUrl(postCancelUrl));
}
}