launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #04554
[Merge] lp:~abentley/launchpad/url-validation into lp:launchpad
Aaron Bentley has proposed merging lp:~abentley/launchpad/url-validation into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #819841 in Launchpad itself: "OOPs rendering page with non-ascii URL"
https://bugs.launchpad.net/launchpad/+bug/819841
Bug #819859 in Launchpad itself: "launchpad urls permit spurious whitespace"
https://bugs.launchpad.net/launchpad/+bug/819859
For more details, see:
https://code.launchpad.net/~abentley/launchpad/url-validation/+merge/71068
= Summary =
Add validation to ensure traversal names are ascii, with no whitesppace
Fixes:
Bug #819841: OOPs rendering page with non-ascii URL
Bug #819859: launchpad urls permit spurious whitespace
== Proposed fix ==
Add validation as the ultimate step of beforeTraversal.
== Pre-implementation notes ==
Discussed with lifeless and gary_poster
== Implementation details ==
Traversal is done using the request's "traversalStack", which are
url-unescaped, unicode path fragments.
It must be done as the final step to ensure the environment is all set up. Otherwise, the normal Launchpad error handling won't work.
== Tests ==
bin/test test_publication
== Demo and Q/A ==
Visit
https://bugs.qastaging.launchpad.net/launchpad/+bug/240067%20
https://bugs.qastaging.launchpad.net/launchpad/+bug/240067%C2%A0
Both urls should give the standard Launchpad "Lost something?" 404 page.
= Launchpad lint =
Checking for conflicts and issues in changed files.
Linting changed files:
lib/canonical/launchpad/webapp/publication.py
lib/canonical/launchpad/webapp/tests/test_publication.py
--
https://code.launchpad.net/~abentley/launchpad/url-validation/+merge/71068
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/url-validation into lp:launchpad.
=== modified file 'lib/canonical/launchpad/webapp/publication.py'
--- lib/canonical/launchpad/webapp/publication.py 2011-07-01 15:36:23 +0000
+++ lib/canonical/launchpad/webapp/publication.py 2011-08-10 15:44:47 +0000
@@ -45,6 +45,7 @@
)
from zope.publisher.interfaces import (
IPublishTraverse,
+ NotFound,
Retry,
)
from zope.publisher.interfaces.browser import (
@@ -83,6 +84,7 @@
IPersonSet,
ITeam,
)
+from lp.services.encoding import is_ascii_only
from lp.services import features
from lp.services.features.flags import NullFeatureController
from lp.services.osutils import open_for_writing
@@ -151,7 +153,7 @@
# exception was added as a result of bug 597324 (message #10 in
# particular).
return
- referrer = request.getHeader('referer') # match HTTP spec misspelling
+ referrer = request.getHeader('referer') # match HTTP spec misspelling
if not referrer:
raise NoReferrerError('No value for REFERER header')
# XXX: jamesh 2007-04-26 bug=98437:
@@ -308,6 +310,19 @@
self.maybeRestrictToTeam(request)
maybe_block_offsite_form_post(request)
self.maybeNotifyReadOnlyMode(request)
+ if not self._validTraversalStack(request):
+ raise NotFound(self.getApplication(request), '')
+
+ @staticmethod
+ def _validTraversalStack(request):
+ """Elements of the traversal stack must be ascii non-whitespace."""
+ whitespace_re = re.compile(r'\s')
+ for element in request.getTraversalStack():
+ if not is_ascii_only(element):
+ return False
+ if whitespace_re.search(element):
+ return False
+ return True
def maybeNotifyReadOnlyMode(self, request):
"""Hook to notify about read-only mode."""
@@ -533,7 +548,7 @@
if request.method in ['GET', 'HEAD']:
self.finishReadOnlyRequest(txn)
elif txn.isDoomed():
- txn.abort() # Sends an abort to the database, even though
+ txn.abort() # Sends an abort to the database, even though
# transaction is still doomed.
else:
txn.commit()
@@ -734,11 +749,11 @@
if IBrowserRequest.providedBy(request):
OpStats.stats['http requests'] += 1
status = request.response.getStatus()
- if status == 404: # Not Found
+ if status == 404: # Not Found
OpStats.stats['404s'] += 1
- elif status == 500: # Unhandled exceptions
+ elif status == 500: # Unhandled exceptions
OpStats.stats['500s'] += 1
- elif status == 503: # Timeouts
+ elif status == 503: # Timeouts
OpStats.stats['503s'] += 1
# Increment counters for status code groups.
=== modified file 'lib/canonical/launchpad/webapp/tests/test_publication.py'
--- lib/canonical/launchpad/webapp/tests/test_publication.py 2011-05-20 04:46:43 +0000
+++ lib/canonical/launchpad/webapp/tests/test_publication.py 2011-08-10 15:44:47 +0000
@@ -18,6 +18,7 @@
)
from storm.exceptions import DisconnectionError
from storm.zope.interfaces import IZStorm
+from testtools.testcase import ExpectedException
from zope.component import getUtility
from zope.error.interfaces import IErrorReportingUtility
from zope.interface import directlyProvides
@@ -42,6 +43,7 @@
remove_read_only_file,
touch_read_only_file,
)
+from canonical.launchpad.webapp import canonical_url
import canonical.launchpad.webapp.adapter as dbadapter
from canonical.launchpad.webapp.interfaces import (
IStoreSelector,
@@ -68,6 +70,7 @@
FunctionalLayer,
)
from lp.testing import (
+ logout,
TestCase,
TestCaseWithFactory,
)
@@ -97,6 +100,30 @@
publication.callTraversalHooks(request, obj2)
self.assertEquals(request.traversed_objects, [obj1])
+ def test_validTraversalStack_ascii_no_space(self):
+ # ascii path with no whitespace is valid.
+ request = LaunchpadTestRequest(PATH_INFO="foo")
+ result = LaunchpadBrowserPublication._validTraversalStack(request)
+ self.assertTrue(result)
+
+ def test_validTraversalStack_space(self):
+ # ascii path with whitespace is invalid.
+ request = LaunchpadTestRequest(PATH_INFO="foo ")
+ result = LaunchpadBrowserPublication._validTraversalStack(request)
+ self.assertFalse(result)
+
+ def test_validTraversalStack_nonascii(self):
+ # path with non-ascii is invalid.
+ request = LaunchpadTestRequest(PATH_INFO=u"foo\xC0".encode('utf-8'))
+ result = LaunchpadBrowserPublication._validTraversalStack(request)
+ self.assertFalse(result)
+
+ def test_validTraversalStack_nonascii_space(self):
+ # path with non-ascii whitespace is invalid.
+ request = LaunchpadTestRequest(PATH_INFO=u"foo\xa0".encode('utf-8'))
+ result = LaunchpadBrowserPublication._validTraversalStack(request)
+ self.assertFalse(result)
+
class TestReadOnlyModeSwitches(TestCase):
# At the beginning of every request (in publication.beforeTraversal()), we
@@ -466,3 +493,26 @@
browser.open,
'http://launchpad.dev/%ED%B4%B5')
self.assertEqual(0, len(self.oopses))
+
+ def test_beforeTraversal_non_ascii_url(self):
+ # The NotFound is raised by beforeTraversal
+ publication = LaunchpadBrowserPublication(None)
+ request = LaunchpadTestRequest(PATH_INFO='\xED\xB4\xB5')
+ logout()
+ with ExpectedException(NotFound, ''):
+ publication.beforeTraversal(request)
+ # Remove database policy created by beforeTraversal
+ getUtility(IStoreSelector).pop()
+
+
+class TestWhitespacePath(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_whitespace_bugtask(self):
+ # Bug tasks should not permit whitespace in their number.
+ bugtask = self.factory.makeBugTask()
+ url = canonical_url(bugtask) + '%20'
+ browser = self.getUserBrowser()
+ with ExpectedException(NotFound, ''):
+ browser.open(url)