-
Notifications
You must be signed in to change notification settings - Fork 2
GitHub Issue #1023: Add redirect() helper that uses core-safeRedirect #2021
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
e7aca92
7.42.3-fb-redirectGH1023.0
cnathe e673a6c
GitHub Issue #1023: Add redirect() helper that uses core-safeRedirect
cnathe b7bd943
useAppNavigate to replace usage of setting window.location.href with …
cnathe fa4bbcd
7.42.3-fb-redirectGH1023.1
cnathe 2a31aee
update comment
cnathe 9029e5c
remove if check
cnathe 7815f92
7.42.3-fb-redirectGH1023.2
cnathe 694edfe
Merge remote-tracking branch 'origin/develop' into fb_redirectGH1023
cnathe 7f87456
7.43.1-fb-redirectGH1023.0
cnathe cd7c23b
Merge remote-tracking branch 'origin/develop' into fb_redirectGH1023
cnathe 32cf66d
7.45.1-fb-redirectGH1023.0
cnathe cd4f57c
update redirect helper to handle more cases - checking for AppURL, ch…
cnathe fea6854
more usages of redirect helper
cnathe 9c4b89a
move isSameOrigin tests from Plate designer
cnathe b4d4256
7.45.1-fb-redirectGH1023.1
cnathe 3bc5a79
Merge remote-tracking branch 'origin/develop' into fb_redirectGH1023
cnathe 7896a53
7.45.2-fb-redirectGH1023.0
cnathe 64bcd70
more usages of redirect helper
cnathe 4c4bea9
7.45.2-fb-redirectGH1023.1
cnathe aa39f87
add cross-container test cases for getRedirectUrl()
cnathe f5c3d8d
prefer usages of navigate() over redirect()
cnathe 45a3e56
npm run lint-branch-fix
cnathe 3bbc17e
update release notes
cnathe ee27ebd
7.45.2
cnathe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| /* | ||
| * Copyright (c) 2026 LabKey Corporation. All rights reserved. No portion of this work may be reproduced | ||
| * in any form or by any electronic or mechanical means without written permission from LabKey Corporation. | ||
| */ | ||
| import { ActionURL } from '@labkey/api'; | ||
|
|
||
| import { getRedirectUrl, isSameOrigin } from './ActionURL'; | ||
| import { AppURL, buildURL } from './AppURL'; | ||
|
|
||
| // window.location.origin is "http://localhost" in the jsdom test environment. | ||
| const safeRedirectUrl = (returnUrl: string): string => | ||
| ActionURL.buildURL('core', 'safeRedirect', undefined, { returnUrl }); | ||
|
|
||
| describe('getRedirectUrl', () => { | ||
| describe('AppURL', () => { | ||
| test('navigates directly to an in-app AppURL via toHref()', () => { | ||
| const url = AppURL.create('registry', 'molecule'); | ||
| expect(url.toHref()).toEqual('#/registry/molecule'); // sanity check: in-app hash path | ||
| expect(getRedirectUrl(url)).toEqual('#/registry/molecule'); | ||
| }); | ||
|
|
||
| test('navigates directly to a cross-app AppURL via toHref(), never through safeRedirect', () => { | ||
| const url = AppURL.create('registry', 'molecule').setProductId('biologics'); | ||
| expect(url.toHref()).not.toMatch(/^#/); // sanity check: full cross-app URL, not a hash path | ||
| expect(getRedirectUrl(url)).toEqual(url.toHref()); | ||
| expect(getRedirectUrl(url)).not.toContain('safeRedirect'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('same-origin string -> navigates directly', () => { | ||
| test.each([ | ||
| ['relative path', '/labkey/MyContainer/some-action.view?rowId=4'], | ||
| ['relative path with hash', '/labkey/MyContainer/app.view#/samples/123'], | ||
| ['absolute same-origin URL', 'http://localhost/labkey/MyContainer/some-action.view'], | ||
| ['root-relative path', '/home/project-begin.view'], | ||
| ['cross-container relative path', '/labkey/OtherProject/SubFolder/some-action.view?rowId=4'], | ||
| ['cross-container nested path', '/labkey/OtherProject/Sub/Deep/Folder/project-begin.view'], | ||
| [ | ||
| 'cross-container absolute same-origin URL', | ||
| 'http://localhost/labkey/OtherProject/SubFolder/app.view#/samples/123', | ||
| ], | ||
| ])('%s', (_label, url) => { | ||
| expect(getRedirectUrl(url)).toEqual(url); | ||
| }); | ||
|
|
||
| test('a buildURL() result navigates directly', () => { | ||
| // Mirrors SamplesEditButton: a self-built server action URL is same-origin, so it is not wrapped. | ||
| const url = buildURL('publish', 'sampleTypePublishStart.view', { rowId: 4 }); | ||
| expect(getRedirectUrl(url)).toEqual(url); | ||
| expect(getRedirectUrl(url)).not.toContain('safeRedirect'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('cross-origin / opaque string -> routes through safeRedirect', () => { | ||
| test.each([ | ||
| ['external https URL', 'https://evil.example.com/phish'], | ||
| ['protocol-relative URL', '//evil.example.com/phish'], | ||
| ['userinfo confusion (real host is evil)', 'https://localhost@evil.example.com/phish'], | ||
| ['backslash trickery', 'https:/\\/\\evil.example.com/phish'], | ||
| ])('%s', (_label, url) => { | ||
| const result = getRedirectUrl(url); | ||
| expect(result).toEqual(safeRedirectUrl(url)); | ||
| expect(result).toContain('core-safeRedirect.view'); | ||
| expect(result).toContain('returnUrl='); | ||
| expect(result).not.toEqual(url); | ||
| }); | ||
|
|
||
| test('javascript: URL has an opaque origin and is routed through safeRedirect', () => { | ||
| const url = 'javascript:alert(1)'; // eslint-disable-line no-script-url | ||
| expect(getRedirectUrl(url)).toEqual(safeRedirectUrl(url)); | ||
| }); | ||
|
|
||
| test('unparseable string is treated as unsafe and routed through safeRedirect', () => { | ||
| const url = 'http://[::::::]:not-a-port'; | ||
| expect(getRedirectUrl(url)).toEqual(safeRedirectUrl(url)); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('isSameOrigin', () => { | ||
| // jsdom sets window.location.origin to 'http://localhost' | ||
|
|
||
| test('returns true for a URL on the same origin', () => { | ||
| expect(isSameOrigin('http://localhost/some/path')).toBe(true); | ||
| }); | ||
|
|
||
| test('returns true for a root-relative URL (same origin by construction)', () => { | ||
| expect(isSameOrigin('/labkey/plate/plateList.view')).toBe(true); | ||
| }); | ||
|
|
||
| test('returns false for a different hostname', () => { | ||
| expect(isSameOrigin('http://evil.com/path')).toBe(false); | ||
| }); | ||
|
|
||
| test('returns false for a different scheme', () => { | ||
| expect(isSameOrigin('https://localhost/path')).toBe(false); | ||
| }); | ||
|
|
||
| test('returns false for a different port', () => { | ||
| expect(isSameOrigin('http://localhost:8080/path')).toBe(false); | ||
| }); | ||
|
|
||
| test('returns false for a javascript: URL (XSS guard)', () => { | ||
| expect(isSameOrigin('javascript:alert(1)')).toBe(false); | ||
| }); | ||
|
|
||
| test('returns false for an absolute URL with an invalid host (throws during construction)', () => { | ||
| // 'http://a b' has a space in the hostname which is invalid; the URL constructor throws, | ||
| // and the catch block returns false. | ||
| expect(isSameOrigin('http://a b/path')).toBe(false); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should add some cross-container URLs to these tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done