Merge ~pelpsi/launchpad:avoid-open-redirect-attack-on-logout into launchpad:master

Proposed by Simone Pelosi
Status: Merged
Approved by: Simone Pelosi
Approved revision: 60cf04570d5ade9b5014bd906f5a0797af504fef
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pelpsi/launchpad:avoid-open-redirect-attack-on-logout
Merge into: launchpad:master
Diff against target: 95 lines (+56/-3)
2 files modified
lib/launchpad_loggerhead/app.py (+8/-2)
lib/launchpad_loggerhead/tests.py (+48/-1)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+439730@code.launchpad.net

Commit message

Restricted user control on next_to redirect

A penetration test found that lougot redirect is vulnerable to open redirect
attack. "next_to" url is now validated: if it belongs to our domains, the
user is redirected to that url, otherwise the user is redirected to
a default url (homepage).

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Needs Fixing
Revision history for this message
Colin Watson (cjwatson) wrote :

Thanks for the correction. Could you also add a test that a request to `config.codehosting.secure_codebrowse_root + "+logout?" + urlencode({"next_to": config.launchpad.openid_provider_root + "+logout"})` works, since that's roughly what `CookieLogoutPage` does? (No need for re-review after adding that.)

review: Approve
Revision history for this message
Simone Pelosi (pelpsi) wrote :

> Thanks for the correction. Could you also add a test that a request to
> `config.codehosting.secure_codebrowse_root + "+logout?" +
> urlencode({"next_to": config.launchpad.openid_provider_root + "+logout"})`
> works, since that's roughly what `CookieLogoutPage` does? (No need for re-
> review after adding that.)
Sure!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/launchpad_loggerhead/app.py b/lib/launchpad_loggerhead/app.py
2index 8326a12..20549b4 100644
3--- a/lib/launchpad_loggerhead/app.py
4+++ b/lib/launchpad_loggerhead/app.py
5@@ -5,7 +5,7 @@ import logging
6 import os
7 import threading
8 import xmlrpc.client
9-from urllib.parse import urlencode, urljoin
10+from urllib.parse import urlencode, urljoin, urlparse
11
12 import oops_wsgi
13 from breezy import errors, lru_cache, urlutils
14@@ -163,7 +163,13 @@ class RootApp:
15 environ[self.session_var].clear()
16 query = dict(parse_querystring(environ))
17 next_url = query.get("next_to")
18- if next_url is None:
19+ next_url_domain = urlparse(next_url).netloc
20+ open_id_url = config.launchpad.openid_provider_root
21+ open_id_domain = urlparse(open_id_url).netloc
22+ if next_url is None or (
23+ next_url_domain not in allvhosts.hostnames
24+ and next_url_domain != open_id_domain
25+ ):
26 next_url = allvhosts.configs["mainsite"].rooturl
27 raise HTTPMovedPermanently(next_url)
28
29diff --git a/lib/launchpad_loggerhead/tests.py b/lib/launchpad_loggerhead/tests.py
30index 560042c..1bc5fae 100644
31--- a/lib/launchpad_loggerhead/tests.py
32+++ b/lib/launchpad_loggerhead/tests.py
33@@ -106,7 +106,7 @@ class TestLogout(TestCase):
34 # TestLoginAndLogout.test_CookieLogoutPage).
35
36 # Here, we will have a more useless example of the basic machinery.
37- dummy_root = "http://dummy.test/"
38+ dummy_root = "http://launchpad.test/"
39 self.browser.open(
40 config.codehosting.secure_codebrowse_root
41 + "+logout?"
42@@ -122,6 +122,53 @@ class TestLogout(TestCase):
43 self.browser.contents, b"This is a dummy destination.\n"
44 )
45
46+ def testLoggerheadLogoutRedirectCookieLogoutMimic(self):
47+ # When we visit +logout with a 'next_to' value in the query string,
48+ # the logout page will redirect to the given URI. As of this
49+ # writing, this is used by Launchpad to redirect to our OpenId
50+ # provider (see lp.testing.tests.test_login.
51+ # TestLoginAndLogout.test_CookieLogoutPage).
52+
53+ # CookieLogout behaviour mimic
54+ self.browser.open(
55+ config.codehosting.secure_codebrowse_root
56+ + "+logout?"
57+ + urlencode(
58+ dict(next_to=config.launchpad.openid_provider_root + "+logout")
59+ )
60+ )
61+
62+ # We are logged out, as before.
63+ self.assertEqual(self.session, {})
64+
65+ # Now, though, we are redirected to the ``next_to`` destination.
66+ self.assertEqual(
67+ self.browser.url, config.launchpad.openid_provider_root + "+logout"
68+ )
69+ self.assertEqual(
70+ self.browser.contents, b"This is a dummy destination.\n"
71+ )
72+
73+ def testLoggerheadLogoutRedirectFailure(self):
74+ # When we visit +logout with a 'next_to' value in the query string,
75+ # the logout page will redirect to the given URI only if
76+ # the url belongs to well known domains
77+
78+ # Here, we will have an example of open redirect attack
79+ dummy_root = "http://launchpad.phishing.test/"
80+ self.browser.open(
81+ config.codehosting.secure_codebrowse_root
82+ + "+logout?"
83+ + urlencode(dict(next_to=dummy_root + "+logout"))
84+ )
85+
86+ # We are logged out, as before.
87+ self.assertEqual(self.session, {})
88+
89+ # We are redirected to the default homepage because
90+ # the next_to is unknown
91+ self.assertEqual(self.browser.url, "http://launchpad.test/")
92+
93
94 class TestWSGI(TestCaseWithFactory):
95 """Smoke tests for Launchpad's loggerhead WSGI server."""

Subscribers

People subscribed via source and target branches

to status/vote changes: