Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/desktop/src/main/net/safe-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function safeFetch(url: string, opts: SafeFetchOptions = {}): Promi
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
try {
const res = await net.fetch(url, { signal: controller.signal });
const res = await net.fetch(url, { signal: controller.signal, redirect: 'error' });
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.

security-medium medium

To prevent potential SSRF bypasses via URL parser differentials, it is safer to use the normalized URL from the parsed object rather than the original url string. This ensures that the URL fetched by net.fetch is exactly the same one that was validated by the protocol and hostname checks. This is a recommended security practice when implementing SSRF guards to avoid discrepancies between how different libraries parse the same URL string.

Suggested change
const res = await net.fetch(url, { signal: controller.signal, redirect: 'error' });
const res = await net.fetch(parsed.href, { signal: controller.signal, redirect: 'error' });
References
  1. The style guide emphasizes guarding URLs and treating the Electron main process as a security boundary. Using normalized URLs prevents bypasses due to differences in how various components parse the same string. (link)

if (!res.ok) throw new Error(`fetch ${url}: ${res.status}`);
const cl = Number(res.headers.get('content-length') ?? '0');
if (cl > maxSize) throw new Error('response too large');
Expand Down
8 changes: 8 additions & 0 deletions packages/desktop/test/safe-fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,12 @@ describe('safeFetch', () => {

await expect(safeFetch('https://cdn.example.com/missing.png')).rejects.toThrow('404');
});

it('passes redirect:error to net.fetch to prevent SSRF bypass via 3xx', async () => {
assertNotPrivateHostMock.mockResolvedValue(undefined);
netFetchMock.mockResolvedValue(mockResponse(new ArrayBuffer(4)));

await safeFetch('https://cdn.example.com/img.png');
expect(netFetchMock).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ redirect: 'error' }));
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.

medium

Instead of using expect.any(String), it is better to assert the exact URL being passed to net.fetch. This ensures that the function is fetching the intended resource and that any normalization (like using parsed.href) is correctly applied, making the test more robust against regressions.

Suggested change
expect(netFetchMock).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ redirect: 'error' }));
expect(netFetchMock).toHaveBeenCalledWith('https://cdn.example.com/img.png', expect.objectContaining({ redirect: 'error' }));

});
});