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
8 changes: 4 additions & 4 deletions assay/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion assay/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
}
},
"dependencies": {
"@labkey/components": "7.40.0"
"@labkey/components": "7.40.1"
},
"devDependencies": {
"@labkey/build": "9.1.4",
Expand Down
8 changes: 4 additions & 4 deletions core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
}
},
"dependencies": {
"@labkey/components": "7.40.0",
"@labkey/components": "7.40.1",
"@labkey/themes": "1.9.3"
},
"devDependencies": {
Expand Down
5 changes: 3 additions & 2 deletions core/src/client/AssayDesigner/AssayDesigner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,9 @@ class AssayDesigner 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
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 @@ -66,9 +66,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 @@ -82,9 +82,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 @@ -103,8 +103,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 @@ -144,9 +144,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.
// 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());
}
}
}
6 changes: 4 additions & 2 deletions core/src/org/labkey/core/analytics/AnalyticsServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public class AnalyticsServiceImpl implements AnalyticsService
{
private static final String SEPARATOR = ",";
private static final String GOOGLE_TAG_MANAGER_URL = "https://www.googletagmanager.com";
private static final String GOOGLE_URL = "https://www.google.com";
private static final String ANALYTICS_CSP_KEY = AnalyticsServiceImpl.class.getName();

public static AnalyticsServiceImpl get()
Expand Down Expand Up @@ -123,8 +124,9 @@ public void resetCSP()

if (getTrackingStatus().contains(TrackingStatus.ga4FullUrl))
{
ContentSecurityPolicyFilter.registerAllowedSources(ANALYTICS_CSP_KEY, Directive.Connection, "https://*.googletagmanager.com", "https://*.google-analytics.com", "https://*.analytics.google.com");
ContentSecurityPolicyFilter.registerAllowedSources(ANALYTICS_CSP_KEY, Directive.Image, "https://www.googletagmanager.com");
// Per https://developers.google.com/tag-platform/security/guides/csp (plus other variants we have seen in the wild)
ContentSecurityPolicyFilter.registerAllowedSources(ANALYTICS_CSP_KEY, Directive.Connection, "https://*.googletagmanager.com", "https://*.google-analytics.com", "https://*.analytics.google.com", GOOGLE_URL);
ContentSecurityPolicyFilter.registerAllowedSources(ANALYTICS_CSP_KEY, Directive.Image, GOOGLE_TAG_MANAGER_URL);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public void testReorderConfigurations()

signOut();
assertSsoLinkOrder(config_uncool, config_cool);
clickAndWait(Locator.button("Sign In"));
clickAndWait(Locator.linkWithText("Sign In"));
assertSsoLinkOrder(config_uncool, config_cool);
simpleSignIn();

Expand All @@ -79,7 +79,7 @@ public void testReorderConfigurations()

signOut();
assertSsoLinkOrder(config_cool, config_uncool);
clickAndWait(Locator.button("Sign In"));
clickAndWait(Locator.linkWithText("Sign In"));
assertSsoLinkOrder(config_cool, config_uncool);
simpleSignIn();
}
Expand Down
8 changes: 4 additions & 4 deletions experiment/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion experiment/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"test-integration": "cross-env NODE_ENV=test jest --ci --runInBand -c test/js/jest.config.integration.js"
},
"dependencies": {
"@labkey/components": "7.40.0"
"@labkey/components": "7.40.1"
},
"devDependencies": {
"@labkey/build": "9.1.4",
Expand Down
8 changes: 4 additions & 4 deletions pipeline/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pipeline/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"build-prod": "npm run clean && cross-env NODE_ENV=production PROD_SOURCE_MAP=source-map webpack --config node_modules/@labkey/build/webpack/prod.config.js --color --progress --profile"
},
"dependencies": {
"@labkey/components": "7.40.0"
"@labkey/components": "7.40.1"
},
"devDependencies": {
"@labkey/build": "9.1.4",
Expand Down
1 change: 0 additions & 1 deletion query/src/org/labkey/query/sql/QIfDefined.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ void setQuerySelect(QuerySelect select)
@Override
public FieldKey getFieldKey()
{
assert null != select;
return ((QExpr)getFirstChild()).getFieldKey();
}

Expand Down
Empty file.
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 @@ -782,4 +783,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));
}
}
Loading