Skip to content

fix: remove unsafe redirect_to from url#273

Open
LucasXu0 wants to merge 3 commits intomainfrom
fix/redirect_to
Open

fix: remove unsafe redirect_to from url#273
LucasXu0 wants to merge 3 commits intomainfrom
fix/redirect_to

Conversation

@LucasXu0
Copy link
Contributor

@LucasXu0 LucasXu0 commented Mar 3, 2026

Description

closes AppFlowy-IO/AppFlowy#8551


Checklist

General

  • I've included relevant documentation or comments for the changes introduced.
  • I've tested the changes in multiple environments (e.g., different browsers, operating systems).

Testing

  • I've added or updated tests to validate the changes introduced for AppFlowy Web.

Feature-Specific

  • For feature additions, I've added a preview (video, screenshot, or demo) in the "Feature Preview" section.
  • I've verified that this feature integrates seamlessly with existing functionality.

Summary by Sourcery

Harden post-login redirection by validating stored and querystring redirect URLs and falling back to the main app route when targets are unsafe or user-specific.

Bug Fixes:

  • Prevent open-redirect and unsafe URL navigation after authentication by only allowing same-origin or relative redirect targets and defaulting to /app otherwise.

Enhancements:

  • Strip unsafe redirectTo parameters from the login page URL while preserving other query parameters.

Tests:

  • Add unit tests covering safe/unsafe redirect URL classification and after-auth redirection behavior, including localStorage cleanup.

LucasXu0 added 2 commits March 3, 2026 13:33
Add isSafeRedirectUrl() to validate that redirectTo targets only
same-origin URLs. Apply the guard in afterAuth() and LoginPage.tsx
to block external-domain redirects (CWE-601, issue #8551).
Replace silent block with in-place URL correction: strip the unsafe
redirectTo param via setSearchParams while preserving all other query
params (email, action, etc.), so the login flow continues uninterrupted.
@sourcery-ai
Copy link

sourcery-ai bot commented Mar 3, 2026

Reviewer's Guide

Implements safe post-authentication redirect handling by validating redirect URLs against the current origin, preventing open redirects, and adds tests plus login-page URL param cleanup to strip unsafe redirect targets.

Sequence diagram for safe post-auth redirect handling in afterAuth

sequenceDiagram
  actor User
  participant Browser
  participant AuthModule as AuthModule_afterAuth
  participant SessionStore as SessionStore_redirectTo

  User->>Browser: Complete authentication
  Browser->>AuthModule: Call afterAuth()
  AuthModule->>SessionStore: getRedirectTo()
  SessionStore-->>AuthModule: redirectTo (encoded or null)
  AuthModule->>SessionStore: clearRedirectTo()
  alt redirectTo present
    AuthModule->>AuthModule: decoded = decodeURIComponent(redirectTo)
    AuthModule->>AuthModule: isSafeRedirectUrl(decoded)?
    alt unsafe decoded
      AuthModule->>Browser: window.location.href = /app
    else safe decoded
      AuthModule->>AuthModule: url = new URL(decoded, window.location.origin)
      AuthModule->>AuthModule: pathname = url.pathname
      alt pathname matches user-specific pattern
        AuthModule->>Browser: window.location.href = /app
      else pathname is root or empty
        AuthModule->>Browser: window.location.href = /app
      else other safe pathname
        AuthModule->>Browser: window.location.href = decoded
      end
    end
  else redirectTo missing
    AuthModule->>Browser: window.location.href = /app
  end
Loading

Class-style diagram for updated sign_in session logic and LoginPage

classDiagram
  class SignInSession {
    +isSafeRedirectUrl(url string) boolean
    +afterAuth() void
    +getRedirectTo() string
    +clearRedirectTo() void
  }

  class LoginPageComponent {
    +redirectTo string
    +useEffect_cleanupRedirectTo() void
  }

  SignInSession <.. LoginPageComponent : uses_isSafeRedirectUrl
  LoginPageComponent ..> SignInSession : relies_on_afterAuth
Loading

File-Level Changes

Change Details Files
Introduce reusable redirect URL safety check and apply it in post-auth flow to prevent open redirects and malformed URLs.
  • Added isSafeRedirectUrl helper that treats only same-origin absolute URLs and non-protocol-relative paths as safe.
  • Updated afterAuth to decode stored redirectTo, validate with isSafeRedirectUrl, and fall back to /app when invalid.
  • Changed URL construction in afterAuth to use window.location.origin as base for parsing and to redirect directly to the original decoded path when safe.
  • Simplified root-path redirect behavior to always send users to /app instead of rewriting the URL object.
src/application/session/sign_in.ts
Sanitize login page query parameters client-side by removing unsafe redirectTo values.
  • Injected isSafeRedirectUrl into LoginPage to validate redirectTo search param on mount.
  • Added useEffect that decodes redirectTo, checks safety, and removes redirectTo from the URL while preserving other query params when unsafe.
  • Switched useSearchParams hook usage to also get setSearch for in-place query param updates.
src/pages/LoginPage.tsx
Add unit tests for redirect safety logic and after-auth redirect behavior, including handling of malicious and malformed URLs.
  • Mocked window.localStorage and window.location in test setup to observe redirects without jsdom restrictions.
  • Added positive and negative test cases for isSafeRedirectUrl covering relative paths, same-origin URLs, external domains, protocol-relative, javascript:, data:, empty, malformed, and cross-host URLs.
  • Added afterAuth tests verifying fallback to /app for unsafe redirects, proper handling of UUID-containing paths and root paths, correct following of safe relative and same-origin URLs, and clearing of redirectTo from localStorage after execution.
src/application/session/__tests__/sign_in.test.ts

Assessment against linked issues

Issue Objective Addressed Explanation
AppFlowy-IO/AppFlowy#8551 Validate the redirectTo parameter during post-authentication redirect so that users are only redirected to trusted internal locations (same-origin or relative paths), preventing open redirects to arbitrary external domains.
AppFlowy-IO/AppFlowy#8551 Sanitize the login/signup URL by removing any unsafe redirectTo query parameter on the Login page while preserving other parameters, so that an attacker cannot persist an external redirect link in the visible URL.
AppFlowy-IO/AppFlowy#8551 Add automated tests to verify the new redirect safety logic (isSafeRedirectUrl and afterAuth) against both safe internal redirects and unsafe external/malformed URLs.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • In LoginPage's useEffect, decodeURIComponent(redirectTo) is applied directly to a query param value which is typically already decoded; consider wrapping this in a try/catch or only decoding when you know it was encoded to avoid runtime errors on malformed % sequences.
  • When calling setSearch, you're mutating the existing URLSearchParams instance (prev.delete('redirectTo'); return prev;); it’s safer to construct and return a new URLSearchParams (e.g. const next = new URLSearchParams(prev); next.delete('redirectTo'); return next;) to avoid subtle issues with in-place mutation.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `LoginPage`'s `useEffect`, `decodeURIComponent(redirectTo)` is applied directly to a query param value which is typically already decoded; consider wrapping this in a try/catch or only decoding when you know it was encoded to avoid runtime errors on malformed `%` sequences.
- When calling `setSearch`, you're mutating the existing `URLSearchParams` instance (`prev.delete('redirectTo'); return prev;`); it’s safer to construct and return a new `URLSearchParams` (e.g. `const next = new URLSearchParams(prev); next.delete('redirectTo'); return next;`) to avoid subtle issues with in-place mutation.

## Individual Comments

### Comment 1
<location path="src/pages/LoginPage.tsx" line_range="31-36" />
<code_context>


+  // Strip unsafe redirectTo from URL immediately, preserving all other params
+  useEffect(() => {
+    if (!redirectTo) return;
+
+    const decodedRedirect = decodeURIComponent(redirectTo);
+
+    if (!isSafeRedirectUrl(decodedRedirect)) {
+      setSearch((prev) => {
+        prev.delete('redirectTo');
+        return prev;
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Avoid mutating the `URLSearchParams` instance returned in `setSearch`'s updater.

The updater receives a `URLSearchParams` used to derive the next state, so mutating it in place and returning the same instance depends on implementation details and shared references. Prefer creating a new instance instead:

```ts
setSearch(prev => {
  const next = new URLSearchParams(prev);
  next.delete('redirectTo');
  return next;
});
```

This keeps updates immutable and avoids subtle state bugs.

```suggestion
    if (!isSafeRedirectUrl(decodedRedirect)) {
      setSearch((prev) => {
        const next = new URLSearchParams(prev);
        next.delete('redirectTo');
        return next;
      });
    }
```
</issue_to_address>

### Comment 2
<location path="src/application/session/sign_in.ts" line_range="79-75" />
<code_context>
+      return;
+    }
+
+    const url = new URL(decoded, window.location.origin);
     const pathname = url.pathname;

</code_context>
<issue_to_address>
**issue (bug_risk):** Root-path redirects now drop query parameters compared to the previous behavior.

Previously, for `https://ourapp.com/?foo=bar`, we set `url.pathname = '/app'` and redirected with `url.toString()`, so `?foo=bar` (and any hash) were kept. In the new `pathname === '/'` branch, redirecting directly to `/app` loses those parts. If we still need to preserve them, this branch should also modify `url.pathname` and redirect via `url.toString()` instead of using a hardcoded path.
</issue_to_address>

### Comment 3
<location path="src/application/session/sign_in.ts" line_range="72" />
<code_context>

   if (redirectTo) {
-    const url = new URL(decodeURIComponent(redirectTo));
+    const decoded = decodeURIComponent(redirectTo);
+
+    if (!isSafeRedirectUrl(decoded)) {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Align `decodeURIComponent` error handling in `afterAuth` with the new usage in the Login page.

`decodeURIComponent(redirectTo)` can throw on malformed input. Please extract a small helper that safely decodes and returns `null` on failure, and reuse it both here and in `LoginPage` so invalid encodings are consistently treated as unsafe redirects.

Suggested implementation:

```typescript
  }
}

export function safeDecodeRedirectParam(value: string): string | null {
  try {
    return decodeURIComponent(value);
  } catch {
    return null;
  }
}

export function afterAuth() {
  const redirectTo = getRedirectTo();

  clearRedirectTo();

  if (redirectTo) {
    const decoded = safeDecodeRedirectParam(redirectTo);

    if (!decoded || !isSafeRedirectUrl(decoded)) {
      window.location.href = '/app';
      return;
    }

    const url = new URL(decoded, window.location.origin);
    const pathname = url.pathname;

    // Check if URL contains workspace/view UUIDs (user-specific paths)
      // Don't redirect to user-specific pages from previous sessions
      window.location.href = '/app';

```

` since that file isn't shown).

Here are the changes:

<file_operations>
<file_operation operation="edit" file_path="src/application/session/sign_in.ts">
<<<<<<< SEARCH
  }
}

export function afterAuth() {
  const redirectTo = getRedirectTo();

  clearRedirectTo();

  if (redirectTo) {
    const decoded = decodeURIComponent(redirectTo);

    if (!isSafeRedirectUrl(decoded)) {
      window.location.href = '/app';
      return;
    }

    const url = new URL(decoded, window.location.origin);
    const pathname = url.pathname;

    // Check if URL contains workspace/view UUIDs (user-specific paths)
      // Don't redirect to user-specific pages from previous sessions
      window.location.href = '/app';
=======
  }
}

export function safeDecodeRedirectParam(value: string): string | null {
  try {
    return decodeURIComponent(value);
  } catch {
    return null;
  }
}

export function afterAuth() {
  const redirectTo = getRedirectTo();

  clearRedirectTo();

  if (redirectTo) {
    const decoded = safeDecodeRedirectParam(redirectTo);

    if (!decoded || !isSafeRedirectUrl(decoded)) {
      window.location.href = '/app';
      return;
    }

    const url = new URL(decoded, window.location.origin);
    const pathname = url.pathname;

    // Check if URL contains workspace/view UUIDs (user-specific paths)
      // Don't redirect to user-specific pages from previous sessions
      window.location.href = '/app';
>>>>>>> REPLACE
</file_operation>
</file_operations>

<additional_changes>
To fully align error handling across the app, the `LoginPage` should be updated to import and use `safeDecodeRedirectParam` instead of calling `decodeURIComponent` directly:

1. Import `safeDecodeRedirectParam` from `src/application/session/sign_in` (or wherever this module is exported from).
2. Replace any `decodeURIComponent(redirectTo)` calls in `LoginPage` with `safeDecodeRedirectParam(redirectTo)`.
3. Treat a `null` return from `safeDecodeRedirectParam` the same as an unsafe redirect (e.g. do not redirect or fall back to a safe default such as `/app`), matching the behavior in `afterAuth`.
If `LoginPage` cannot depend on this module for architectural reasons, move `safeDecodeRedirectParam` into a shared utility module and import it from both places instead.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- Add safeDecodeRedirectParam helper (returns null on malformed input)
  and use it in afterAuth and LoginPage instead of bare decodeURIComponent
- Restore query-param/hash preservation for root-path redirects in afterAuth
- Use immutable URLSearchParams construction in LoginPage setSearch updater
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Open Redirect on SignUp Page

1 participant