2
2
mirror of https://github.com/Llewellynvdm/nativefier.git synced 2025-01-05 15:12:11 +00:00

Add --block-external-urls flag to forbid external navigation attempts (Fix #978 - PR#1012)

Fixes #978 

Adds a `--block-external-urls` option (default: `false`) that prevents opening external links (as classified by the `--internal-urls` option).

Documentation and tests updated.


Example:
```
nativefier --internal-urls "classroom\.google\.com" --block-external-urls
```
![image](https://user-images.githubusercontent.com/12286274/88739501-f12d5180-d0f7-11ea-9821-86f3e9bfa070.png)
![image](https://user-images.githubusercontent.com/12286274/88739512-fab6b980-d0f7-11ea-877c-7bd565352a93.png)
This commit is contained in:
Joe Skeen 2020-08-02 12:31:47 -06:00 committed by GitHub
parent 5d9a7ae4bc
commit 8e8cd24e0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 120 additions and 2 deletions

View File

@ -210,10 +210,23 @@ export function createMainWindow(
const getCurrentUrl = (): void => const getCurrentUrl = (): void =>
withFocusedWindow((focusedWindow) => focusedWindow.webContents.getURL()); withFocusedWindow((focusedWindow) => focusedWindow.webContents.getURL());
const onBlockedExternalUrl = (url: string) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
dialog.showMessageBox(mainWindow, {
message: `Cannot navigate to external URL: ${url}`,
type: 'error',
title: 'Navigation blocked',
});
};
const onWillNavigate = (event: Event, urlToGo: string): void => { const onWillNavigate = (event: Event, urlToGo: string): void => {
if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) { if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) {
event.preventDefault(); event.preventDefault();
shell.openExternal(urlToGo); // eslint-disable-line @typescript-eslint/no-floating-promises if (options.blockExternalUrls) {
onBlockedExternalUrl(urlToGo);
} else {
shell.openExternal(urlToGo); // eslint-disable-line @typescript-eslint/no-floating-promises
}
} }
}; };
@ -282,6 +295,8 @@ export function createMainWindow(
createAboutBlankWindow, createAboutBlankWindow,
nativeTabsSupported, nativeTabsSupported,
createNewTab, createNewTab,
options.blockExternalUrls,
onBlockedExternalUrl,
); );
}; };

View File

@ -5,6 +5,7 @@ const internalUrl = 'https://medium.com/topics/technology';
const externalUrl = 'https://www.wikipedia.org/wiki/Electron'; const externalUrl = 'https://www.wikipedia.org/wiki/Electron';
const foregroundDisposition = 'foreground-tab'; const foregroundDisposition = 'foreground-tab';
const backgroundDisposition = 'background-tab'; const backgroundDisposition = 'background-tab';
const blockExternal = false;
const nativeTabsSupported = () => true; const nativeTabsSupported = () => true;
const nativeTabsNotSupported = () => false; const nativeTabsNotSupported = () => false;
@ -14,6 +15,8 @@ test('internal urls should not be handled', () => {
const openExternal = jest.fn(); const openExternal = jest.fn();
const createAboutBlankWindow = jest.fn(); const createAboutBlankWindow = jest.fn();
const createNewTab = jest.fn(); const createNewTab = jest.fn();
const onBlockedExternalUrl = jest.fn();
onNewWindowHelper( onNewWindowHelper(
internalUrl, internalUrl,
undefined, undefined,
@ -24,11 +27,15 @@ test('internal urls should not be handled', () => {
createAboutBlankWindow, createAboutBlankWindow,
nativeTabsNotSupported, nativeTabsNotSupported,
createNewTab, createNewTab,
blockExternal,
onBlockedExternalUrl,
); );
expect(openExternal.mock.calls.length).toBe(0); expect(openExternal.mock.calls.length).toBe(0);
expect(createAboutBlankWindow.mock.calls.length).toBe(0); expect(createAboutBlankWindow.mock.calls.length).toBe(0);
expect(createNewTab.mock.calls.length).toBe(0); expect(createNewTab.mock.calls.length).toBe(0);
expect(preventDefault.mock.calls.length).toBe(0); expect(preventDefault.mock.calls.length).toBe(0);
expect(onBlockedExternalUrl.mock.calls.length).toBe(0);
}); });
test('external urls should be opened externally', () => { test('external urls should be opened externally', () => {
@ -36,6 +43,8 @@ test('external urls should be opened externally', () => {
const createAboutBlankWindow = jest.fn(); const createAboutBlankWindow = jest.fn();
const createNewTab = jest.fn(); const createNewTab = jest.fn();
const preventDefault = jest.fn(); const preventDefault = jest.fn();
const onBlockedExternalUrl = jest.fn();
onNewWindowHelper( onNewWindowHelper(
externalUrl, externalUrl,
undefined, undefined,
@ -46,11 +55,44 @@ test('external urls should be opened externally', () => {
createAboutBlankWindow, createAboutBlankWindow,
nativeTabsNotSupported, nativeTabsNotSupported,
createNewTab, createNewTab,
blockExternal,
onBlockedExternalUrl,
); );
expect(openExternal.mock.calls.length).toBe(1); expect(openExternal.mock.calls.length).toBe(1);
expect(createAboutBlankWindow.mock.calls.length).toBe(0); expect(createAboutBlankWindow.mock.calls.length).toBe(0);
expect(createNewTab.mock.calls.length).toBe(0); expect(createNewTab.mock.calls.length).toBe(0);
expect(preventDefault.mock.calls.length).toBe(1); expect(preventDefault.mock.calls.length).toBe(1);
expect(onBlockedExternalUrl.mock.calls.length).toBe(0);
});
test('external urls should be ignored if blockExternal is true', () => {
const openExternal = jest.fn();
const createAboutBlankWindow = jest.fn();
const createNewTab = jest.fn();
const preventDefault = jest.fn();
const onBlockedExternalUrl = jest.fn();
const blockExternal = true;
onNewWindowHelper(
externalUrl,
undefined,
originalUrl,
undefined,
preventDefault,
openExternal,
createAboutBlankWindow,
nativeTabsNotSupported,
createNewTab,
blockExternal,
onBlockedExternalUrl,
);
expect(openExternal.mock.calls.length).toBe(0);
expect(createAboutBlankWindow.mock.calls.length).toBe(0);
expect(createNewTab.mock.calls.length).toBe(0);
expect(preventDefault.mock.calls.length).toBe(1);
expect(onBlockedExternalUrl.mock.calls.length).toBe(1);
}); });
test('tab disposition should be ignored if tabs are not enabled', () => { test('tab disposition should be ignored if tabs are not enabled', () => {
@ -58,6 +100,8 @@ test('tab disposition should be ignored if tabs are not enabled', () => {
const openExternal = jest.fn(); const openExternal = jest.fn();
const createAboutBlankWindow = jest.fn(); const createAboutBlankWindow = jest.fn();
const createNewTab = jest.fn(); const createNewTab = jest.fn();
const onBlockedExternalUrl = jest.fn();
onNewWindowHelper( onNewWindowHelper(
internalUrl, internalUrl,
foregroundDisposition, foregroundDisposition,
@ -68,11 +112,15 @@ test('tab disposition should be ignored if tabs are not enabled', () => {
createAboutBlankWindow, createAboutBlankWindow,
nativeTabsNotSupported, nativeTabsNotSupported,
createNewTab, createNewTab,
blockExternal,
onBlockedExternalUrl,
); );
expect(openExternal.mock.calls.length).toBe(0); expect(openExternal.mock.calls.length).toBe(0);
expect(createAboutBlankWindow.mock.calls.length).toBe(0); expect(createAboutBlankWindow.mock.calls.length).toBe(0);
expect(createNewTab.mock.calls.length).toBe(0); expect(createNewTab.mock.calls.length).toBe(0);
expect(preventDefault.mock.calls.length).toBe(0); expect(preventDefault.mock.calls.length).toBe(0);
expect(onBlockedExternalUrl.mock.calls.length).toBe(0);
}); });
test('tab disposition should be ignored if url is external', () => { test('tab disposition should be ignored if url is external', () => {
@ -80,6 +128,8 @@ test('tab disposition should be ignored if url is external', () => {
const createAboutBlankWindow = jest.fn(); const createAboutBlankWindow = jest.fn();
const createNewTab = jest.fn(); const createNewTab = jest.fn();
const preventDefault = jest.fn(); const preventDefault = jest.fn();
const onBlockedExternalUrl = jest.fn();
onNewWindowHelper( onNewWindowHelper(
externalUrl, externalUrl,
foregroundDisposition, foregroundDisposition,
@ -90,11 +140,15 @@ test('tab disposition should be ignored if url is external', () => {
createAboutBlankWindow, createAboutBlankWindow,
nativeTabsSupported, nativeTabsSupported,
createNewTab, createNewTab,
blockExternal,
onBlockedExternalUrl,
); );
expect(openExternal.mock.calls.length).toBe(1); expect(openExternal.mock.calls.length).toBe(1);
expect(createAboutBlankWindow.mock.calls.length).toBe(0); expect(createAboutBlankWindow.mock.calls.length).toBe(0);
expect(createNewTab.mock.calls.length).toBe(0); expect(createNewTab.mock.calls.length).toBe(0);
expect(preventDefault.mock.calls.length).toBe(1); expect(preventDefault.mock.calls.length).toBe(1);
expect(onBlockedExternalUrl.mock.calls.length).toBe(0);
}); });
test('foreground tabs with internal urls should be opened in the foreground', () => { test('foreground tabs with internal urls should be opened in the foreground', () => {
@ -102,6 +156,8 @@ test('foreground tabs with internal urls should be opened in the foreground', ()
const createAboutBlankWindow = jest.fn(); const createAboutBlankWindow = jest.fn();
const createNewTab = jest.fn(); const createNewTab = jest.fn();
const preventDefault = jest.fn(); const preventDefault = jest.fn();
const onBlockedExternalUrl = jest.fn();
onNewWindowHelper( onNewWindowHelper(
internalUrl, internalUrl,
foregroundDisposition, foregroundDisposition,
@ -112,12 +168,16 @@ test('foreground tabs with internal urls should be opened in the foreground', ()
createAboutBlankWindow, createAboutBlankWindow,
nativeTabsSupported, nativeTabsSupported,
createNewTab, createNewTab,
blockExternal,
onBlockedExternalUrl,
); );
expect(openExternal.mock.calls.length).toBe(0); expect(openExternal.mock.calls.length).toBe(0);
expect(createAboutBlankWindow.mock.calls.length).toBe(0); expect(createAboutBlankWindow.mock.calls.length).toBe(0);
expect(createNewTab.mock.calls.length).toBe(1); expect(createNewTab.mock.calls.length).toBe(1);
expect(createNewTab.mock.calls[0][1]).toBe(true); expect(createNewTab.mock.calls[0][1]).toBe(true);
expect(preventDefault.mock.calls.length).toBe(1); expect(preventDefault.mock.calls.length).toBe(1);
expect(onBlockedExternalUrl.mock.calls.length).toBe(0);
}); });
test('background tabs with internal urls should be opened in background tabs', () => { test('background tabs with internal urls should be opened in background tabs', () => {
@ -125,6 +185,8 @@ test('background tabs with internal urls should be opened in background tabs', (
const createAboutBlankWindow = jest.fn(); const createAboutBlankWindow = jest.fn();
const createNewTab = jest.fn(); const createNewTab = jest.fn();
const preventDefault = jest.fn(); const preventDefault = jest.fn();
const onBlockedExternalUrl = jest.fn();
onNewWindowHelper( onNewWindowHelper(
internalUrl, internalUrl,
backgroundDisposition, backgroundDisposition,
@ -135,12 +197,16 @@ test('background tabs with internal urls should be opened in background tabs', (
createAboutBlankWindow, createAboutBlankWindow,
nativeTabsSupported, nativeTabsSupported,
createNewTab, createNewTab,
blockExternal,
onBlockedExternalUrl,
); );
expect(openExternal.mock.calls.length).toBe(0); expect(openExternal.mock.calls.length).toBe(0);
expect(createAboutBlankWindow.mock.calls.length).toBe(0); expect(createAboutBlankWindow.mock.calls.length).toBe(0);
expect(createNewTab.mock.calls.length).toBe(1); expect(createNewTab.mock.calls.length).toBe(1);
expect(createNewTab.mock.calls[0][1]).toBe(false); expect(createNewTab.mock.calls[0][1]).toBe(false);
expect(preventDefault.mock.calls.length).toBe(1); expect(preventDefault.mock.calls.length).toBe(1);
expect(onBlockedExternalUrl.mock.calls.length).toBe(0);
}); });
test('about:blank urls should be handled', () => { test('about:blank urls should be handled', () => {
@ -148,6 +214,8 @@ test('about:blank urls should be handled', () => {
const openExternal = jest.fn(); const openExternal = jest.fn();
const createAboutBlankWindow = jest.fn(); const createAboutBlankWindow = jest.fn();
const createNewTab = jest.fn(); const createNewTab = jest.fn();
const onBlockedExternalUrl = jest.fn();
onNewWindowHelper( onNewWindowHelper(
'about:blank', 'about:blank',
undefined, undefined,
@ -158,9 +226,13 @@ test('about:blank urls should be handled', () => {
createAboutBlankWindow, createAboutBlankWindow,
nativeTabsNotSupported, nativeTabsNotSupported,
createNewTab, createNewTab,
blockExternal,
onBlockedExternalUrl,
); );
expect(openExternal.mock.calls.length).toBe(0); expect(openExternal.mock.calls.length).toBe(0);
expect(createAboutBlankWindow.mock.calls.length).toBe(1); expect(createAboutBlankWindow.mock.calls.length).toBe(1);
expect(createNewTab.mock.calls.length).toBe(0); expect(createNewTab.mock.calls.length).toBe(0);
expect(preventDefault.mock.calls.length).toBe(1); expect(preventDefault.mock.calls.length).toBe(1);
expect(onBlockedExternalUrl.mock.calls.length).toBe(0);
}); });

View File

@ -10,10 +10,16 @@ export function onNewWindowHelper(
createAboutBlankWindow, createAboutBlankWindow,
nativeTabsSupported, nativeTabsSupported,
createNewTab, createNewTab,
blockExternal: boolean,
onBlockedExternalUrl: (url: string) => void,
): void { ): void {
if (!linkIsInternal(targetUrl, urlToGo, internalUrls)) { if (!linkIsInternal(targetUrl, urlToGo, internalUrls)) {
openExternal(urlToGo);
preventDefault(); preventDefault();
if (blockExternal) {
onBlockedExternalUrl(urlToGo);
} else {
openExternal(urlToGo);
}
} else if (urlToGo === 'about:blank') { } else if (urlToGo === 'about:blank') {
const newWindow = createAboutBlankWindow(); const newWindow = createAboutBlankWindow();
preventDefault(newWindow); preventDefault(newWindow);

View File

@ -43,6 +43,7 @@
- [[enable-es3-apis]](#enable-es3-apis) - [[enable-es3-apis]](#enable-es3-apis)
- [[insecure]](#insecure) - [[insecure]](#insecure)
- [[internal-urls]](#internal-urls) - [[internal-urls]](#internal-urls)
- [[block-external-urls]](#block-external-urls)
- [[proxy-rules]](#proxy-rules) - [[proxy-rules]](#proxy-rules)
- [[flash]](#flash) - [[flash]](#flash)
- [[flash-path]](#flash-path) - [[flash-path]](#flash-path)
@ -379,6 +380,21 @@ Or, if you want to allow all domains for example for external auths,
nativefier https://google.com --internal-urls ".*?" nativefier https://google.com --internal-urls ".*?"
``` ```
#### [block-external-urls]
```
--block-external-urls
```
Forbid navigation to URLs not considered "internal" (see '--internal-urls'). Instead of opening in an external browser, attempts to navigate to external URLs will be blocked, and an error message will be shown. Default: false
Example:
```bash
nativefier https://google.com --internal-urls ".*?\.google\.*?" --block-external-urls
```
Blocks navigation to any URLs except Google and its subdomains.
#### [proxy-rules] #### [proxy-rules]
@ -785,6 +801,8 @@ var options = {
ignoreCertificate: false, ignoreCertificate: false,
ignoreGpuBlacklist: false, ignoreGpuBlacklist: false,
enableEs3Apis: false, enableEs3Apis: false,
internalUrls: '.*?', // defaults to URLs on same second-level domain as app
blockExternalUrls: false,
insecure: false, insecure: false,
honest: false, honest: false,
zoom: 1.0, zoom: 1.0,

View File

@ -45,6 +45,7 @@ function pickElectronAppArgs(options: AppOptions): any {
ignoreGpuBlacklist: options.nativefier.ignoreGpuBlacklist, ignoreGpuBlacklist: options.nativefier.ignoreGpuBlacklist,
insecure: options.nativefier.insecure, insecure: options.nativefier.insecure,
internalUrls: options.nativefier.internalUrls, internalUrls: options.nativefier.internalUrls,
blockExternalUrls: options.nativefier.blockExternalUrls,
maxHeight: options.nativefier.maxHeight, maxHeight: options.nativefier.maxHeight,
maximize: options.nativefier.maximize, maximize: options.nativefier.maximize,
maxWidth: options.nativefier.maxWidth, maxWidth: options.nativefier.maxWidth,

View File

@ -224,6 +224,10 @@ if (require.main === module) {
'--internal-urls <value>', '--internal-urls <value>',
'regex of URLs to consider "internal"; all other URLs will be opened in an external browser. Default: URLs on same second-level domain as app', 'regex of URLs to consider "internal"; all other URLs will be opened in an external browser. Default: URLs on same second-level domain as app',
) )
.option(
'--block-external-urls',
`forbid navigation to URLs not considered "internal" (see '--internal-urls'). Instead of opening in an external browser, attempts to navigate to external URLs will be blocked. Default: false`,
)
.option( .option(
'--proxy-rules <value>', '--proxy-rules <value>',
'proxy rules; see https://www.electronjs.org/docs/api/session#sessetproxyconfig', 'proxy rules; see https://www.electronjs.org/docs/api/session#sessetproxyconfig',

View File

@ -33,6 +33,7 @@ export interface AppOptions {
inject: string[]; inject: string[];
insecure: boolean; insecure: boolean;
internalUrls: string; internalUrls: string;
blockExternalUrls: boolean;
maximize: boolean; maximize: boolean;
nativefierVersion: string; nativefierVersion: string;
processEnvs: string; processEnvs: string;

View File

@ -71,6 +71,7 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
inject: rawOptions.inject || [], inject: rawOptions.inject || [],
insecure: rawOptions.insecure || false, insecure: rawOptions.insecure || false,
internalUrls: rawOptions.internalUrls || null, internalUrls: rawOptions.internalUrls || null,
blockExternalUrls: rawOptions.blockExternalUrls || false,
maximize: rawOptions.maximize || false, maximize: rawOptions.maximize || false,
nativefierVersion: packageJson.version, nativefierVersion: packageJson.version,
processEnvs: rawOptions.processEnvs, processEnvs: rawOptions.processEnvs,