fix: skip upgrade-insecure-requests CSP for external HTTP backends (#7714)

Signed-off-by: fresh3nough <fresh3nough@users.noreply.github.com>
Signed-off-by: fre <anonwurcod@proton.me>
This commit is contained in:
fre$h
2026-03-13 07:30:35 +00:00
committed by GitHub
parent e9f00fc216
commit b475ecfc09
3 changed files with 215 additions and 41 deletions
+5 -41
View File
@@ -51,6 +51,7 @@ import { Client } from './api/client';
import { GooseApp } from './api';
import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer';
import { BLOCKED_PROTOCOLS, WEB_PROTOCOLS } from './utils/urlSecurity';
import { buildCSP } from './utils/csp';
function shouldSetupUpdater(): boolean {
// Setup updater if either the flag is enabled OR dev updates are enabled
@@ -1778,51 +1779,14 @@ async function appMain() {
}
});
const buildConnectSrc = (): string => {
const sources = [
"'self'",
'http://127.0.0.1:*',
'https://127.0.0.1:*',
'http://localhost:*',
'https://localhost:*',
'https://api.github.com',
'https://github.com',
'https://objects.githubusercontent.com',
];
const settings = getSettings();
if (settings.externalGoosed?.enabled && settings.externalGoosed.url) {
try {
const externalUrl = new URL(settings.externalGoosed.url);
sources.push(externalUrl.origin);
} catch {
console.warn('Invalid external goosed URL in settings, skipping CSP entry');
}
}
return sources.join(' ');
};
// Add CSP headers to all sessions
// Add CSP headers to all sessions — recomputed on every response so that
// changes to externalGoosed settings take effect without restarting the app.
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
const currentSettings = getSettings();
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy':
"default-src 'self';" +
"style-src 'self' 'unsafe-inline';" +
"script-src 'self' 'unsafe-inline';" +
"img-src 'self' data: https:;" +
`connect-src ${buildConnectSrc()};` +
"object-src 'none';" +
"frame-src 'self' https: http:;" +
"font-src 'self' data: https:;" +
"media-src 'self' mediastream:;" +
"form-action 'none';" +
"base-uri 'self';" +
"manifest-src 'self';" +
"worker-src 'self';" +
'upgrade-insecure-requests;',
'Content-Security-Policy': buildCSP(currentSettings.externalGoosed),
},
});
});
+135
View File
@@ -0,0 +1,135 @@
import { describe, it, expect } from 'vitest';
import { buildConnectSrc, shouldUpgradeInsecureRequests, buildCSP } from '../csp';
import type { ExternalGoosedConfig } from '../settings';
describe('buildConnectSrc', () => {
it('includes default sources when no external backend is configured', () => {
const result = buildConnectSrc(undefined);
expect(result).toContain("'self'");
expect(result).toContain('http://127.0.0.1:*');
});
it('includes external backend origin when enabled', () => {
const config: ExternalGoosedConfig = {
enabled: true,
url: 'http://dev.company.net:12604',
secret: 'test',
};
const result = buildConnectSrc(config);
expect(result).toContain('http://dev.company.net:12604');
});
it('does not include external origin when disabled', () => {
const config: ExternalGoosedConfig = {
enabled: false,
url: 'http://dev.company.net:12604',
secret: 'test',
};
const result = buildConnectSrc(config);
expect(result).not.toContain('dev.company.net');
});
it('handles invalid URLs gracefully', () => {
const config: ExternalGoosedConfig = {
enabled: true,
url: 'not-a-valid-url',
secret: 'test',
};
const result = buildConnectSrc(config);
expect(result).toContain("'self'");
expect(result).not.toContain('not-a-valid-url');
});
});
describe('shouldUpgradeInsecureRequests', () => {
it('returns true when no external backend is configured', () => {
expect(shouldUpgradeInsecureRequests(undefined)).toBe(true);
});
it('returns true when external backend is disabled', () => {
const config: ExternalGoosedConfig = {
enabled: false,
url: 'http://dev.company.net:12604',
secret: 'test',
};
expect(shouldUpgradeInsecureRequests(config)).toBe(true);
});
it('returns false when external backend uses HTTP', () => {
const config: ExternalGoosedConfig = {
enabled: true,
url: 'http://dev.company.net:12604',
secret: 'test',
};
expect(shouldUpgradeInsecureRequests(config)).toBe(false);
});
it('returns true when external backend uses HTTPS', () => {
const config: ExternalGoosedConfig = {
enabled: true,
url: 'https://dev.company.net:12604',
secret: 'test',
};
expect(shouldUpgradeInsecureRequests(config)).toBe(true);
});
it('returns true for invalid URLs', () => {
const config: ExternalGoosedConfig = {
enabled: true,
url: 'not-a-url',
secret: 'test',
};
expect(shouldUpgradeInsecureRequests(config)).toBe(true);
});
it('returns true when URL is empty', () => {
const config: ExternalGoosedConfig = {
enabled: true,
url: '',
secret: 'test',
};
expect(shouldUpgradeInsecureRequests(config)).toBe(true);
});
});
describe('buildCSP', () => {
it('includes upgrade-insecure-requests with no external backend', () => {
const csp = buildCSP(undefined);
expect(csp).toContain('upgrade-insecure-requests');
});
it('includes upgrade-insecure-requests with HTTPS external backend', () => {
const config: ExternalGoosedConfig = {
enabled: true,
url: 'https://secure.company.net:12604',
secret: 'test',
};
const csp = buildCSP(config);
expect(csp).toContain('upgrade-insecure-requests');
expect(csp).toContain('https://secure.company.net:12604');
});
it('excludes upgrade-insecure-requests with HTTP external backend', () => {
const config: ExternalGoosedConfig = {
enabled: true,
url: 'http://dev.company.net:12604',
secret: 'test',
};
const csp = buildCSP(config);
expect(csp).not.toContain('upgrade-insecure-requests');
expect(csp).toContain('http://dev.company.net:12604');
});
it('always includes core directives', () => {
const config: ExternalGoosedConfig = {
enabled: true,
url: 'http://dev.company.net:12604',
secret: 'test',
};
const csp = buildCSP(config);
expect(csp).toContain("default-src 'self'");
expect(csp).toContain("script-src 'self' 'unsafe-inline'");
expect(csp).toContain('connect-src');
expect(csp).toContain("object-src 'none'");
});
});
+75
View File
@@ -0,0 +1,75 @@
import type { ExternalGoosedConfig } from './settings';
const DEFAULT_CONNECT_SOURCES = [
"'self'",
'http://127.0.0.1:*',
'https://127.0.0.1:*',
'http://localhost:*',
'https://localhost:*',
'https://api.github.com',
'https://github.com',
'https://objects.githubusercontent.com',
];
export function buildConnectSrc(externalGoosed?: ExternalGoosedConfig): string {
const sources = [...DEFAULT_CONNECT_SOURCES];
if (externalGoosed?.enabled && externalGoosed.url) {
try {
const externalUrl = new URL(externalGoosed.url);
sources.push(externalUrl.origin);
} catch {
console.warn('Invalid external goosed URL in settings, skipping CSP entry');
}
}
return sources.join(' ');
}
/**
* Returns true when upgrade-insecure-requests should be included in the CSP.
*
* The directive is omitted when the user has configured an external backend
* that uses plain HTTP, because Chromium would silently rewrite those
* requests to HTTPS. The remote server typically does not speak TLS, so the
* upgraded requests fail with "Failed to fetch".
*
* Loopback addresses (127.0.0.1 / localhost) are exempt from the upgrade
* per the CSP spec, which is why the built-in local backend is unaffected.
*/
export function shouldUpgradeInsecureRequests(externalGoosed?: ExternalGoosedConfig): boolean {
if (!externalGoosed?.enabled || !externalGoosed.url) {
return true;
}
try {
const parsed = new URL(externalGoosed.url);
return parsed.protocol !== 'http:';
} catch {
return true;
}
}
export function buildCSP(externalGoosed?: ExternalGoosedConfig): string {
const connectSrc = buildConnectSrc(externalGoosed);
const upgradeDirective = shouldUpgradeInsecureRequests(externalGoosed)
? 'upgrade-insecure-requests;'
: '';
return (
"default-src 'self';" +
"style-src 'self' 'unsafe-inline';" +
"script-src 'self' 'unsafe-inline';" +
"img-src 'self' data: https:;" +
`connect-src ${connectSrc};` +
"object-src 'none';" +
"frame-src 'self' https: http:;" +
"font-src 'self' data: https:;" +
"media-src 'self' mediastream:;" +
"form-action 'none';" +
"base-uri 'self';" +
"manifest-src 'self';" +
"worker-src 'self';" +
upgradeDirective
);
}