From 7f6fbc906a21e9f8db36e06ace2a9b687aa26130 Mon Sep 17 00:00:00 2001
From: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue, 23 Feb 2016 10:51:54 +0100
Subject: [PATCH] Prevented static file corruption when URL fragment contains
 '..'.

When running collectstatic with a hashing static file storage backend,
URLs referencing other files were normalized with posixpath.normpath.
This could corrupt URLs: for example 'a.css#b/../c' became just 'c'.

Normalization seems to be an artifact of the historical implementation.
It contained a home-grown implementation of posixpath.join which relied
on counting occurrences of .. and /, so multiple / had to be collapsed.

The new implementation introduced in the previous commit doesn't suffer
from this issue. So it seems safe to remove the normalization.

There was a test for this normalization behavior but I don't think it's
a good test. Django shouldn't modify CSS that way. If a developer has
rendundant /s, it's mostly an aesthetic issue and it isn't Django's job
to fix it. Conversely, if the user wants a series of /s, perhaps in the
URL fragment, Django shouldn't destroy it.

Refs #26249.
---
 django/contrib/staticfiles/storage.py              |  8 --------
 .../project/documents/cached/css/fragments.css     |  2 +-
 .../project/documents/cached/denorm.css            |  4 ----
 tests/staticfiles_tests/test_storage.py            | 14 ++------------
 4 files changed, 3 insertions(+), 25 deletions(-)
 delete mode 100644 tests/staticfiles_tests/project/documents/cached/denorm.css

diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py
index 636ba0da5f..8949969c58 100644
--- a/django/contrib/staticfiles/storage.py
+++ b/django/contrib/staticfiles/storage.py
@@ -170,14 +170,6 @@ class HashedFilesMixin(object):
             if url.startswith('/') and not url.startswith(settings.STATIC_URL):
                 return matched
 
-            # This is technically not useful and could be considered a bug:
-            # we're making changes to our user's code for no good reason.
-            # Removing it makes test_template_tag_denorm fail, though, and I'm
-            # working on another bug, so I'm going to leave it there for now.
-            # When someone complains that /foo/bar#a/../b gets changed to
-            # /foo/bar#b, just remove it, as well as test_template_tag_denorm.
-            url = posixpath.normpath(url)
-
             # Strip off the fragment so a path-like fragment won't interfere.
             url_path, fragment = urldefrag(url)
 
diff --git a/tests/staticfiles_tests/project/documents/cached/css/fragments.css b/tests/staticfiles_tests/project/documents/cached/css/fragments.css
index e6e7049465..533d7617aa 100644
--- a/tests/staticfiles_tests/project/documents/cached/css/fragments.css
+++ b/tests/staticfiles_tests/project/documents/cached/css/fragments.css
@@ -1,7 +1,7 @@
 @font-face {
     src: url('fonts/font.eot?#iefix') format('embedded-opentype'),
          url('fonts/font.svg#webfontIyfZbseF') format('svg');
-         url('fonts/font.svg#../path/to/fonts/font.svg') format('svg');
+         url('fonts/font.svg#path/to/../../fonts/font.svg') format('svg');
          url('data:font/woff;charset=utf-8;base64,d09GRgABAAAAADJoAA0AAAAAR2QAAQAAAAAAAAAAAAA');
 }
 div {
diff --git a/tests/staticfiles_tests/project/documents/cached/denorm.css b/tests/staticfiles_tests/project/documents/cached/denorm.css
deleted file mode 100644
index d6567b00dd..0000000000
--- a/tests/staticfiles_tests/project/documents/cached/denorm.css
+++ /dev/null
@@ -1,4 +0,0 @@
-@import url("..//cached///styles.css");
-body {
-    background: #d3d6d8 url(img/relative.png );
-}
diff --git a/tests/staticfiles_tests/test_storage.py b/tests/staticfiles_tests/test_storage.py
index 9de5a28223..edb5339bcf 100644
--- a/tests/staticfiles_tests/test_storage.py
+++ b/tests/staticfiles_tests/test_storage.py
@@ -85,12 +85,12 @@ class TestHashedFiles(object):
 
     def test_path_with_querystring_and_fragment(self):
         relpath = self.hashed_file_path("cached/css/fragments.css")
-        self.assertEqual(relpath, "cached/css/fragments.ef92012a8c16.css")
+        self.assertEqual(relpath, "cached/css/fragments.59dc2b188043.css")
         with storage.staticfiles_storage.open(relpath) as relfile:
             content = relfile.read()
             self.assertIn(b'fonts/font.a4b0478549d0.eot?#iefix', content)
             self.assertIn(b'fonts/font.b8d603e42714.svg#webfontIyfZbseF', content)
-            self.assertIn(b'fonts/font.b8d603e42714.svg#../path/to/fonts/font.svg', content)
+            self.assertIn(b'fonts/font.b8d603e42714.svg#path/to/../../fonts/font.svg', content)
             self.assertIn(b'data:font/woff;charset=utf-8;base64,d09GRgABAAAAADJoAA0AAAAAR2QAAQAAAAAAAAAAAAA', content)
             self.assertIn(b'#default#VML', content)
 
@@ -116,16 +116,6 @@ class TestHashedFiles(object):
             self.assertNotIn(b"/static/styles_root.css", content)
             self.assertIn(b"/static/styles_root.401f2509a628.css", content)
 
-    def test_template_tag_denorm(self):
-        relpath = self.hashed_file_path("cached/denorm.css")
-        self.assertEqual(relpath, "cached/denorm.c5bd139ad821.css")
-        with storage.staticfiles_storage.open(relpath) as relfile:
-            content = relfile.read()
-            self.assertNotIn(b"..//cached///styles.css", content)
-            self.assertIn(b"../cached/styles.bb84a0240107.css", content)
-            self.assertNotIn(b"url(img/relative.png )", content)
-            self.assertIn(b'url("img/relative.acae32e4532b.png', content)
-
     def test_template_tag_relative(self):
         relpath = self.hashed_file_path("cached/relative.css")
         self.assertEqual(relpath, "cached/relative.b0375bd89156.css")