Fix open redirect vulnerability in return-to URL after login
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -61,10 +61,15 @@ export const useLoginStore = defineStore('loginStore', {
|
|||||||
// 則在此情況下時,我們會在使用者稍後登入後,把使用者帶到剛才記住的 return-to 網址
|
// 則在此情況下時,我們會在使用者稍後登入後,把使用者帶到剛才記住的 return-to 網址
|
||||||
if(this.rememberedReturnToUrl !== "") {
|
if(this.rememberedReturnToUrl !== "") {
|
||||||
const decodedUrl = atob(this.rememberedReturnToUrl);
|
const decodedUrl = atob(this.rememberedReturnToUrl);
|
||||||
|
// Only allow relative paths to prevent open redirect attacks
|
||||||
|
if(decodedUrl.startsWith('/') && !decodedUrl.startsWith('//')) {
|
||||||
window.location.href = decodedUrl;
|
window.location.href = decodedUrl;
|
||||||
} else {
|
} else {
|
||||||
this.$router.push('/files');
|
this.$router.push('/files');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.$router.push('/files');
|
||||||
|
}
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
this.isInvalid = true;
|
this.isInvalid = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -107,6 +107,29 @@ describe('loginStore', () => {
|
|||||||
window.location = originalLocation;
|
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 () => {
|
it('sets isInvalid on error', async () => {
|
||||||
axios.post.mockRejectedValue(new Error('401'));
|
axios.post.mockRejectedValue(new Error('401'));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user