Skip to content

[WEB-4943] refactor: enhance URL validation and redirection logic in authentication views#7815

Merged
sriramveeraghanta merged 2 commits intopreviewfrom
fix-empty-next-path
Sep 17, 2025
Merged

[WEB-4943] refactor: enhance URL validation and redirection logic in authentication views#7815
sriramveeraghanta merged 2 commits intopreviewfrom
fix-empty-next-path

Conversation

@pablohashescobar
Copy link
Member

@pablohashescobar pablohashescobar commented Sep 17, 2025

Description

  • Updated authentication views (SignInAuthSpaceEndpoint, GitHubCallbackSpaceEndpoint, GitLabCallbackSpaceEndpoint, GoogleCallbackSpaceEndpoint, and MagicSignInSpaceEndpoint) to include url_has_allowed_host_and_scheme checks for safer redirection.
  • Improved URL construction by ensuring proper formatting and fallback to base host when necessary.
  • Added get_allowed_hosts function to path_validator.py for better host validation.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Screenshots and Media (if applicable)

Test Scenarios

References

WEB-4943

Summary by CodeRabbit

  • Bug Fixes
    • Hardened post-authentication redirects across GitHub, GitLab, Google, and email/magic-link flows by enforcing allowed-host checks and falling back to the base site when needed.
    • Next_path is applied only when validated, reducing unexpected or broken redirects.
    • Query parameters are preserved on fallback redirects to avoid losing context during login/signup.
    • Minor URL formatting consistency for redirects; no functional impact.

…ion views

* Updated authentication views (SignInAuthSpaceEndpoint, GitHubCallbackSpaceEndpoint, GitLabCallbackSpaceEndpoint, GoogleCallbackSpaceEndpoint, and MagicSignInSpaceEndpoint) to include url_has_allowed_host_and_scheme checks for safer redirection.
* Improved URL construction by ensuring proper formatting and fallback to base host when necessary.
* Added get_allowed_hosts function to path_validator.py for better host validation.
Copilot AI review requested due to automatic review settings September 17, 2025 09:23
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 17, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds allowed-host validation to space OAuth redirects (GitHub, GitLab, Google) using Django's url_has_allowed_host_and_scheme with get_allowed_hosts; refactors get_allowed_hosts and get_safe_redirect_url to assemble query parts and prefer netlocs; minor rstrip/import style tweaks in email and magic endpoints.

Changes

Cohort / File(s) Summary of Changes
Auth Space Views: Redirect guardrails
apps/api/plane/authentication/views/space/github.py, apps/api/plane/authentication/views/space/gitlab.py, apps/api/plane/authentication/views/space/google.py
Add allowed-host checks via url_has_allowed_host_and_scheme(..., allowed_hosts=get_allowed_hosts()) before performing redirects; fall back to base host when disallowed; import updates and minor rstrip style tweaks.
Auth Space Views: Email & Magic tweaks
apps/api/plane/authentication/views/space/email.py, apps/api/plane/authentication/views/space/magic.py
email.py: import get_allowed_hosts() (no behavioral change); both files: minor .rstrip('/') stylistic adjustments only.
Path validator utilities
apps/api/plane/utils/path_validator.py
get_allowed_hosts() now returns host netlocs (host[:port]) collected from configured origins; get_safe_redirect_url() reworked to centralize query assembly, include validated next_path only when valid, preserve encoded params on fallback, and avoid double-slash before query.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant V as Auth View (Initiate/Callback)
  participant PV as path_validator.get_safe_redirect_url
  participant DH as Django url_has_allowed_host_and_scheme
  participant GA as path_validator.get_allowed_hosts

  C->>V: Request (next_path, params)
  V->>PV: Build safe_url = get_safe_redirect_url(base_url, next_path, params)
  PV-->>V: safe_url
  V->>GA: get_allowed_hosts()
  V->>DH: url_has_allowed_host_and_scheme(safe_url, allowed_hosts)
  alt Allowed
    V-->>C: Redirect to safe_url
  else Disallowed
    V-->>C: Redirect to base_host (preserve encoded params if present)
  end
Loading
sequenceDiagram
  autonumber
  participant PV as get_safe_redirect_url
  participant VA as validate_next_path
  participant ENC as urlencode(params)

  PV->>VA: validate next_path
  VA-->>PV: validated_path or empty
  PV->>ENC: urlencode(params) if params
  ENC-->>PV: encoded_params
  PV->>PV: assemble query_parts = [validated_path?, encoded_params?]
  PV-->>Caller: return base_url[?query_parts] or base_url?encoded_params on disallowed next_path
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Suggested labels

🛡️security, ready to merge

Suggested reviewers

  • dheeru0198
  • sriramveeraghanta

Poem

A rabbit sniffs the redirect trail,
Nets hosts, trims paths, secures each mail.
If hosts misbehave, back to the den—
Params kept safe, and hops again. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The PR description gives a clear summary of the changes, lists affected endpoints, marks the change type, and links the related ticket, but it omits the required "Test Scenarios" content from the repository template and does not document what tests were run or how reviewers can reproduce and verify the behavior. Please populate the "Test Scenarios" section with the steps you executed to verify the fix (manual reproduction steps, unit/integration tests added or commands to run, and expected vs actual outcomes) and add any relevant logs or screenshots; once test details are provided the description will conform to the repository template.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "[WEB-4943] refactor: enhance URL validation and redirection logic in authentication views" accurately and concisely summarizes the primary change set—adding allowed-host checks and improving redirect construction across multiple authentication endpoints—and includes the ticket ID for traceability, making it clear to reviewers what the PR's main intent is.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-empty-next-path

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@makeplane
Copy link

makeplane bot commented Sep 17, 2025

Pull Request Linked with Plane Work Items

Comment Automatically Generated by Plane

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR enhances URL validation and redirection logic in authentication views to improve security by implementing proper host validation and safe URL construction.

  • Added url_has_allowed_host_and_scheme checks to all authentication callback endpoints
  • Improved URL construction with proper query parameter handling in get_safe_redirect_url
  • Enhanced get_allowed_hosts function to extract netloc from base origin URLs

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
apps/api/plane/utils/path_validator.py Enhanced host validation and URL construction logic
apps/api/plane/authentication/views/space/magic.py Added host validation checks and fixed string quotes
apps/api/plane/authentication/views/space/google.py Added URL validation with fallback redirection
apps/api/plane/authentication/views/space/gitlab.py Added URL validation with fallback redirection
apps/api/plane/authentication/views/space/github.py Added URL validation with fallback redirection
apps/api/plane/authentication/views/space/email.py Added host validation import and fixed string quotes

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@cursor
Copy link

cursor bot commented Sep 17, 2025

You have run out of free Bugbot PR reviews for this billing cycle. This will reset on September 20.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

* Updated comments for clarity in the get_safe_redirect_url function.
* Removed unnecessary blank line to enhance
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/api/plane/authentication/views/space/google.py (1)

61-61: Bug: base_host function shadowed by local variable (TypeError risk)

Line 61 assigns base_host = request.session.get("host"), shadowing the imported function. Subsequent calls to base_host(...) will crash.

Apply this diff to remove the shadowing (the variable is unused):

-        base_host = request.session.get("host")
+        # host saved during initiate is available via request.session["host"] if needed
apps/api/plane/authentication/views/space/github.py (1)

61-61: Bug: base_host function shadowed by local variable (TypeError risk)

Same issue as Google callback: assigning base_host = request.session.get("host") shadows the imported function and will break later calls.

Apply this diff:

-        base_host = request.session.get("host")
+        # host saved during initiate is available via request.session["host"] if needed
apps/api/plane/authentication/views/space/gitlab.py (1)

62-76: Critical: base_host name shadowing breaks redirects.

At Line 62, base_host (string) shadows the imported function, causing 'str' object is not callable at Lines 72/85/107.

Apply:

-        base_host = request.session.get("host")
+        session_base_host = request.session.get("host")
@@
-            url = get_safe_redirect_url(
-                base_url=base_host(request=request, is_space=True),
+            url = get_safe_redirect_url(
+                base_url=session_base_host or base_host(request=request, is_space=True),
                 next_path=next_path,
                 params=params
             )
@@
-            url = get_safe_redirect_url(
-                base_url=base_host(request=request, is_space=True),
+            url = get_safe_redirect_url(
+                base_url=session_base_host or base_host(request=request, is_space=True),
                 next_path=next_path,
                 params=params
             )
@@
-            url = get_safe_redirect_url(
-                base_url=base_host(request=request, is_space=True),
+            url = get_safe_redirect_url(
+                base_url=session_base_host or base_host(request=request, is_space=True),
                 next_path=next_path,
                 params=params
             )
🧹 Nitpick comments (8)
apps/api/plane/utils/path_validator.py (3)

110-110: Remove unused import

quote is unused after the refactor.

Apply this diff:

-    from urllib.parse import urlencode, quote
+    from urllib.parse import urlencode

99-99: Avoid mutable default for params

Use a None default to prevent accidental state bleed.

Apply this diff:

-def get_safe_redirect_url(base_url: str, next_path: str = "", params: dict = {}) -> str:
+def get_safe_redirect_url(base_url: str, next_path: str = "", params: dict | None = None) -> str:

No further changes needed since the body already guards with truthiness checks.


50-62: Filter empty hosts and deduplicate allowed_hosts

urlparse(...).netloc can be empty if a URL lacks a scheme. Also dedupe to avoid redundant checks.

Apply this diff:

-    allowed_hosts = []
-    if base_origin:
-        host = urlparse(base_origin).netloc
-        allowed_hosts.append(host)
+    allowed_hosts = []
+    if base_origin:
+        host = urlparse(base_origin).netloc
+        if host:
+            allowed_hosts.append(host)
     if settings.ADMIN_BASE_URL:
         # Get only the host
         host = urlparse(settings.ADMIN_BASE_URL).netloc
-        allowed_hosts.append(host)
+        if host:
+            allowed_hosts.append(host)
     if settings.SPACE_BASE_URL:
         # Get only the host
         host = urlparse(settings.SPACE_BASE_URL).netloc
-        allowed_hosts.append(host)
-    return allowed_hosts
+        if host:
+            allowed_hosts.append(host)
+    # Preserve order while deduping
+    return list(dict.fromkeys(allowed_hosts))
apps/api/plane/authentication/views/space/google.py (2)

24-25: next_path read but not persisted to session

Initiate reads next_path but doesn’t store it for use in the callback. If callbacks rely on session-stored next_path, persist it here.

Apply this diff:

         next_path = request.GET.get("next_path")
+        request.session["next_path"] = next_path

21-47: Optional: tighten HTTPS requirement when appropriate

If deployments enforce HTTPS, consider passing require_https=True to url_has_allowed_host_and_scheme via a helper to align with security posture. Leave off in dev.

I can wire this through a small utility that consults settings.SECURE_SSL_REDIRECT.

apps/api/plane/authentication/views/space/email.py (1)

103-109: Inconsistent post-sign-in redirect style vs other endpoints

Here you redirect to base_url with ?next_path=..., while other flows redirect directly to base_url + next_path. Consider unifying for predictable UX (either always direct-path or always query-param).

I can submit a follow-up diff to align this with the direct-path pattern plus allowed-host check.

apps/api/plane/authentication/views/space/github.py (1)

24-26: next_path read but not persisted to session

Initiate reads next_path but doesn’t save it for the callback. Persist if callbacks depend on it.

Apply this diff:

         request.session["host"] = base_host(request=request, is_space=True)
         next_path = request.GET.get("next_path")
+        request.session["next_path"] = next_path
apps/api/plane/authentication/views/space/gitlab.py (1)

99-103: Allow‑list check: good; add https requirement and consistent base URL usage.

Use the session host when available and require https when the request is secure.

Apply:

-            url = f"{base_host(request=request, is_space=True).rstrip('/')}{next_path}"
-            if url_has_allowed_host_and_scheme(url, allowed_hosts=get_allowed_hosts()):
-                return HttpResponseRedirect(url)
-            else:
-                return HttpResponseRedirect(base_host(request=request, is_space=True))
+            base_origin = (session_base_host or base_host(request=request, is_space=True)).rstrip("/")
+            url = f"{base_origin}{next_path or '/'}"
+            if url_has_allowed_host_and_scheme(
+                url,
+                allowed_hosts=get_allowed_hosts(),
+                require_https=request.is_secure(),
+            ):
+                return HttpResponseRedirect(url)
+            return HttpResponseRedirect(base_origin + "/")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6d3d9e6 and e09749f.

📒 Files selected for processing (6)
  • apps/api/plane/authentication/views/space/email.py (2 hunks)
  • apps/api/plane/authentication/views/space/github.py (3 hunks)
  • apps/api/plane/authentication/views/space/gitlab.py (3 hunks)
  • apps/api/plane/authentication/views/space/google.py (3 hunks)
  • apps/api/plane/authentication/views/space/magic.py (2 hunks)
  • apps/api/plane/utils/path_validator.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
apps/api/plane/authentication/views/space/google.py (1)
apps/api/plane/utils/path_validator.py (3)
  • get_safe_redirect_url (99-144)
  • validate_next_path (65-96)
  • get_allowed_hosts (46-62)
apps/api/plane/authentication/views/space/github.py (1)
apps/api/plane/utils/path_validator.py (3)
  • get_safe_redirect_url (99-144)
  • validate_next_path (65-96)
  • get_allowed_hosts (46-62)
apps/api/plane/authentication/views/space/email.py (1)
apps/api/plane/utils/path_validator.py (3)
  • get_safe_redirect_url (99-144)
  • validate_next_path (65-96)
  • get_allowed_hosts (46-62)
apps/api/plane/authentication/views/space/gitlab.py (1)
apps/api/plane/utils/path_validator.py (3)
  • get_safe_redirect_url (99-144)
  • get_allowed_hosts (46-62)
  • validate_next_path (65-96)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (8)
apps/api/plane/authentication/views/space/magic.py (1)

99-103: Redirect allowed-host guardrails look good

Using url_has_allowed_host_and_scheme with get_allowed_hosts and falling back to base_host is correct and closes open-redirect vectors.

Also applies to: 161-165

apps/api/plane/utils/path_validator.py (1)

144-144: Confirm intent: dropping next_path on fallback

On disallowed URLs you return base_url with params but omit next_path. If UX expects preserving the intended destination (still as a query param on the safe origin), consider keeping next_path encoded in the fallback too.

Would you like me to adjust fallback to include next_path as a query param on base_url?

apps/api/plane/authentication/views/space/google.py (1)

95-99: LGTM: safe redirect with allowed-host check

Correctly validates the constructed absolute URL and falls back to base host.

apps/api/plane/authentication/views/space/email.py (1)

202-206: LGTM: post-signup redirect hardened

Allowed-host check with fallback is correct and consistent with other endpoints.

apps/api/plane/authentication/views/space/github.py (1)

98-102: LGTM: safe redirect with allowed-host check

The guard plus fallback looks correct.

apps/api/plane/authentication/views/space/gitlab.py (3)

7-7: Import for host allow‑listing: looks good.

Correct utility for validating redirect targets.


18-18: Centralized validators import: good call.

Keeps all redirect safety helpers in one place.


25-26: Persist next_path across the OAuth round‑trip.

Callback reads request.session["next_path"], but initiate view doesn’t set it here.

If not set elsewhere, add:

         request.session["host"] = base_host(request=request, is_space=True)
         next_path = request.GET.get("next_path")
+        request.session["next_path"] = next_path

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/plane/utils/path_validator.py (1)

99-99: Avoid mutable default for params.

Use None default and normalize inside.

-def get_safe_redirect_url(base_url: str, next_path: str = "", params: dict = {}) -> str:
+def get_safe_redirect_url(base_url: str, next_path: str = "", params: dict | None = None) -> str:

(Initialization added in the previous diff: params = params or {}.)

♻️ Duplicate comments (2)
apps/api/plane/utils/path_validator.py (2)

143-143: Preserve the full query (including next_path) on fallback.

Current fallback only keeps encoded_params; it drops next_path. Reuse the computed query_string.

-    return base_url + (f"?{encoded_params}" if encoded_params else "")
+    return f"{base_url}?{query_string}" if query_string else base_url

118-137: Encode next_path; build the query once; and drop the '/?' (it mutates the path).

  • next_path is appended unencoded and can break the query.
  • '/?' inserts an extra slash, potentially changing routes (…/callback → …/callback/).
  • urlencode called without doseq mishandles list values.

Apply:

-    # Prepare the query parameters
-    query_parts = []
-    encoded_params = ""
-
-    # Add the next path to the parameters
-    if validated_path:
-        query_parts.append(f"next_path={validated_path}")
-
-    # Add additional parameters
-    if params:
-        encoded_params = urlencode(params)
-        query_parts.append(encoded_params)
-
-    # Construct the url query string
-    if query_parts:
-        query_string = "&".join(query_parts)
-        url = f"{base_url}/?{query_string}"
-    else:
-        url = base_url
+    # Assemble query once (encode next_path; support multi-value params)
+    params = params or {}
+    merged_params = dict(params)  # shallow copy
+    if validated_path:
+        # ensure encoding by funneling through urlencode
+        merged_params["next_path"] = validated_path
+    query_string = urlencode(merged_params, doseq=True)
+    url = f"{base_url}?{query_string}" if query_string else base_url
🧹 Nitpick comments (3)
apps/api/plane/utils/path_validator.py (3)

50-62: Normalize and de‑dupe allowed hosts; handle URLs without a scheme.

urlparse("example.com") yields empty netloc; also duplicates and case variance can creep in.

-    allowed_hosts = []
-    if base_origin:
-        host = urlparse(base_origin).netloc
-        allowed_hosts.append(host)
-    if settings.ADMIN_BASE_URL:
-        # Get only the host
-        host = urlparse(settings.ADMIN_BASE_URL).netloc
-        allowed_hosts.append(host)
-    if settings.SPACE_BASE_URL:
-        # Get only the host
-        host = urlparse(settings.SPACE_BASE_URL).netloc
-        allowed_hosts.append(host)
-    return allowed_hosts
+    allowed_hosts: list[str] = []
+    for candidate in [base_origin, settings.ADMIN_BASE_URL, settings.SPACE_BASE_URL]:
+        if not candidate:
+            continue
+        parsed = urlparse(candidate.strip())
+        host = (parsed.netloc or parsed.path).lower()  # tolerate missing scheme
+        if host:
+            allowed_hosts.append(host)
+    # de-dup while preserving order
+    return list(dict.fromkeys(allowed_hosts))

110-110: Remove unused import.

quote is not used.

-    from urllib.parse import urlencode, quote
+    from urllib.parse import urlencode

139-141: Optionally restrict schemes.

If production requires HTTPS, pass allowed_schemes={"https"}.

Would you like me to gate this by settings (e.g., SECURE_SSL_REDIRECT) and wire allowed_schemes accordingly?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e09749f and d09cc14.

📒 Files selected for processing (1)
  • apps/api/plane/utils/path_validator.py (2 hunks)

@sriramveeraghanta sriramveeraghanta merged commit 3d06189 into preview Sep 17, 2025
7 of 10 checks passed
@sriramveeraghanta sriramveeraghanta deleted the fix-empty-next-path branch September 17, 2025 10:43
yarikoptic pushed a commit to yarikoptic/plane that referenced this pull request Oct 1, 2025
…authentication views (makeplane#7815)

* refactor: enhance URL validation and redirection logic in authentication views

* Updated authentication views (SignInAuthSpaceEndpoint, GitHubCallbackSpaceEndpoint, GitLabCallbackSpaceEndpoint, GoogleCallbackSpaceEndpoint, and MagicSignInSpaceEndpoint) to include url_has_allowed_host_and_scheme checks for safer redirection.
* Improved URL construction by ensuring proper formatting and fallback to base host when necessary.
* Added get_allowed_hosts function to path_validator.py for better host validation.

* refactor: improve comments and clean up code in path_validator.py

* Updated comments for clarity in the get_safe_redirect_url function.
* Removed unnecessary blank line to enhance
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.

4 participants