From eea79c852bd574142828c3289af0b48d5433ba2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BE=9D=E7=91=AA=E8=B2=93?= Date: Sat, 7 Mar 2026 08:13:12 +0800 Subject: [PATCH] Fix open redirect vulnerability in return-to URL after login Co-Authored-By: Claude Opus 4.6 --- src/stores/login.ts | 7 ++++++- tests/stores/login.test.js | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/stores/login.ts b/src/stores/login.ts index fd4ff3f..67cdb1b 100644 --- a/src/stores/login.ts +++ b/src/stores/login.ts @@ -61,7 +61,12 @@ export const useLoginStore = defineStore('loginStore', { // 則在此情況下時,我們會在使用者稍後登入後,把使用者帶到剛才記住的 return-to 網址 if(this.rememberedReturnToUrl !== "") { const decodedUrl = atob(this.rememberedReturnToUrl); - window.location.href = decodedUrl; + // Only allow relative paths to prevent open redirect attacks + if(decodedUrl.startsWith('/') && !decodedUrl.startsWith('//')) { + window.location.href = decodedUrl; + } else { + this.$router.push('/files'); + } } else { this.$router.push('/files'); } diff --git a/tests/stores/login.test.js b/tests/stores/login.test.js index 4b74dbc..e282103 100644 --- a/tests/stores/login.test.js +++ b/tests/stores/login.test.js @@ -107,6 +107,29 @@ describe('loginStore', () => { window.location = originalLocation; }); + it('does not redirect to external URL (open redirect prevention)', async () => { + axios.post.mockResolvedValue({ + data: { + access_token: 'token', + refresh_token: 'refresh', + }, + }); + // Attacker crafts a return-to URL pointing to an external site + store.rememberedReturnToUrl = btoa('https://evil.example.com/steal'); + + const originalLocation = window.location; + delete window.location; + window.location = { href: '' }; + + await store.signIn(); + + // Should NOT redirect to the external URL + expect(window.location.href).not.toBe('https://evil.example.com/steal'); + // Should fall back to /files + expect(store.$router.push).toHaveBeenCalledWith('/files'); + window.location = originalLocation; + }); + it('sets isInvalid on error', async () => { axios.post.mockRejectedValue(new Error('401'));