Skip to content

Commit

Permalink
Allow to integrate subresource-integrity attributes to javascript and…
Browse files Browse the repository at this point in the history
… stylesheet tags.

Require to add the two following keys beside "output_filename" in Django's setting PIPELINE['JAVASCRIPT']["your package"] and PIPELINE['STYLESHEETS']["your package"] and :

"crossorigin": "anonymous",
"integrity": "sha384",
Of course, "sha256" and "sha512" also works.
Hashes are computed at runtime and cached to minimize code changes.

Cf. https://infosec.mozilla.org/guidelines/web_security#subresource-integrity
  • Loading branch information
d9pouces committed Jul 5, 2024
1 parent 980aad4 commit 9fd17d6
Show file tree
Hide file tree
Showing 13 changed files with 321 additions and 7 deletions.
19 changes: 19 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,25 @@ A dictionary passed to compiler's ``compile_file`` method as kwargs. None of def

Defaults to ``{}``.

``crossorigin``
...............

**Optional**

Indicate if you want to add to the group this attribute that provides support for CORS, defining how the element handles cross-origin requests, thereby enabling the configuration of the CORS requests for the element's fetched data. .

Missing by default (the attribute is not added), the only valid values currently are ``anonymous`` and ``use-credentials``.

``integrity``
.............

**Optional**

Indicate if you want to add the sub-resource integrity (SRI) attribute to the group.
This attribute contains inline metadata that a user agent can use to verify that a fetched resource has been delivered free of unexpected manipulation

Missing by default, and only valid values are ``"sha256"``, ``"sha384"`` and ``"sha512"``.


Other settings
--------------
Expand Down
9 changes: 8 additions & 1 deletion pipeline/jinja2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ def render_css(self, package, path):
template_name = package.template_name or "pipeline/css.jinja"
context = package.extra_context
context.update(
{"type": guess_type(path, "text/css"), "url": staticfiles_storage.url(path)}
{
"type": guess_type(path, "text/css"),
"url": staticfiles_storage.url(path),
"crossorigin": package.config.get("crossorigin"),
"integrity": package.get_sri(path),
}
)
template = self.environment.get_template(template_name)
return template.render(context)
Expand All @@ -66,6 +71,8 @@ def render_js(self, package, path):
{
"type": guess_type(path, "text/javascript"),
"url": staticfiles_storage.url(path),
"crossorigin": package.config.get("crossorigin"),
"integrity": package.get_sri(path),
}
)
template = self.environment.get_template(template_name)
Expand Down
2 changes: 1 addition & 1 deletion pipeline/jinja2/pipeline/css.jinja
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<link href="{{ url }}" rel="stylesheet" type="{{ type }}"{% if media %} media="{{ media }}"{% endif %}{% if charset %} charset="{{ charset }}"{% endif %} />
<link href="{{ url }}" rel="stylesheet" type="{{ type }}"{% if media %} media="{{ media }}"{% endif %}{% if charset %} charset="{{ charset }}"{% endif %}{% if integrity %} integrity="{{ integrity }}"{% endif %}{% if crossorigin %} crossorigin="{{ crossorigin }}"{% endif %} />
2 changes: 1 addition & 1 deletion pipeline/jinja2/pipeline/js.jinja
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<script{% if async %} async{% endif %}{% if defer %} defer{% endif %} type="{{ type }}" src="{{ url }}" charset="utf-8"></script>
<script{% if async %} async{% endif %}{% if defer %} defer{% endif %} type="{{ type }}" src="{{ url }}" charset="utf-8"{% if integrity %} integrity="{{ integrity }}"{% endif %}{% if crossorigin %} crossorigin="{{ crossorigin }}"{% endif %}></script>
18 changes: 18 additions & 0 deletions pipeline/packager.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import base64
import hashlib
from functools import lru_cache

from django.contrib.staticfiles.finders import find, get_finders
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.files.base import ContentFile
Expand Down Expand Up @@ -61,6 +65,20 @@ def manifest(self):
def compiler_options(self):
return self.config.get("compiler_options", {})

@lru_cache
def get_sri(self, path):
method = self.config.get("integrity")
if method not in {"sha256", "sha384", "sha512"}:
return None
if staticfiles_storage.exists(path):
with staticfiles_storage.open(path) as fd:
h = getattr(hashlib, method)()
for data in iter(lambda: fd.read(16384), b""):
h.update(data)
digest = base64.b64encode(h.digest()).decode()
return f"{method}-{digest}"
return None


class Packager:
def __init__(
Expand Down
2 changes: 1 addition & 1 deletion pipeline/templates/pipeline/css.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<link href="{{ url }}" rel="stylesheet" type="{{ type }}" media="{{ media|default:"all" }}"{% if title %} title="{{ title }}"{% endif %}{% if charset %} charset="{{ charset }}"{% endif %} />
<link href="{{ url }}" rel="stylesheet" type="{{ type }}" media="{{ media|default:"all" }}"{% if title %} title="{{ title }}"{% endif %}{% if charset %} charset="{{ charset }}"{% endif %}{% if integrity %} integrity="{{ integrity }}"{% endif %}{% if crossorigin %} crossorigin="{{ crossorigin }}"{% endif %} />
2 changes: 1 addition & 1 deletion pipeline/templates/pipeline/css.jinja
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<link href="{{ url }}" rel="stylesheet" type="{{ type }}"{% if media %} media="{{ media }}"{% endif %}{% if charset %} charset="{{ charset }}"{% endif %} />
<link href="{{ url }}" rel="stylesheet" type="{{ type }}"{% if media %} media="{{ media }}"{% endif %}{% if charset %} charset="{{ charset }}"{% endif %}{% if integrity %} integrity="{{ integrity }}"{% endif %}{% if crossorigin %} crossorigin="{{ crossorigin }}"{% endif %} />
2 changes: 1 addition & 1 deletion pipeline/templates/pipeline/js.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<script{% if async %} async{% endif %}{% if defer %} defer{% endif %} type="{{ type }}" src="{{ url }}" charset="utf-8"></script>
<script{% if async %} async{% endif %}{% if defer %} defer{% endif %} type="{{ type }}" src="{{ url }}" charset="utf-8"{% if integrity %} integrity="{{ integrity }}"{% endif %}{% if crossorigin %} crossorigin="{{ crossorigin }}"{% endif %}></script>
2 changes: 1 addition & 1 deletion pipeline/templates/pipeline/js.jinja
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<script{% if async %} async{% endif %}{% if defer %} defer{% endif %} type="{{ type }}" src="{{ url }}" charset="utf-8"></script>
<script{% if async %} async{% endif %}{% if defer %} defer{% endif %} type="{{ type }}" src="{{ url }}" charset="utf-8"{% if integrity %} integrity="{{ integrity }}"{% endif %}{% if crossorigin %} crossorigin="{{ crossorigin }}"{% endif %}></script>
4 changes: 4 additions & 0 deletions pipeline/templatetags/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ def render_css(self, package, path):
{
"type": guess_type(path, "text/css"),
"url": mark_safe(staticfiles_storage.url(path)),
"crossorigin": package.config.get("crossorigin"),
"integrity": package.get_sri(path),
}
)
return render_to_string(template_name, context)
Expand Down Expand Up @@ -188,6 +190,8 @@ def render_js(self, package, path):
{
"type": guess_type(path, "text/javascript"),
"url": mark_safe(staticfiles_storage.url(path)),
"crossorigin": package.config.get("crossorigin"),
"integrity": package.get_sri(path),
}
)
return render_to_string(template_name, context)
Expand Down
76 changes: 76 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,42 @@ def local_path(path):
"title": "Default Style",
},
},
"screen_crossorigin": {
"source_filenames": (
"pipeline/css/first.css",
"pipeline/css/second.css",
"pipeline/css/urls.css",
),
"output_filename": "screen_crossorigin.css",
"crossorigin": "anonymous",
},
"screen_sri_sha256": {
"source_filenames": (
"pipeline/css/first.css",
"pipeline/css/second.css",
"pipeline/css/urls.css",
),
"output_filename": "screen_sri_sha256.css",
"integrity": "sha256",
},
"screen_sri_sha384": {
"source_filenames": (
"pipeline/css/first.css",
"pipeline/css/second.css",
"pipeline/css/urls.css",
),
"output_filename": "screen_sri_sha384.css",
"integrity": "sha384",
},
"screen_sri_sha512": {
"source_filenames": (
"pipeline/css/first.css",
"pipeline/css/second.css",
"pipeline/css/urls.css",
),
"output_filename": "screen_sri_sha512.css",
"integrity": "sha512",
},
},
"JAVASCRIPT": {
"scripts": {
Expand Down Expand Up @@ -137,6 +173,46 @@ def local_path(path):
"defer": True,
},
},
"scripts_crossorigin": {
"source_filenames": (
"pipeline/js/first.js",
"pipeline/js/second.js",
"pipeline/js/application.js",
"pipeline/templates/**/*.jst",
),
"output_filename": "scripts_crossorigin.js",
"crossorigin": "anonymous",
},
"scripts_sri_sha256": {
"source_filenames": (
"pipeline/js/first.js",
"pipeline/js/second.js",
"pipeline/js/application.js",
"pipeline/templates/**/*.jst",
),
"output_filename": "scripts_sha256.js",
"integrity": "sha256",
},
"scripts_sri_sha384": {
"source_filenames": (
"pipeline/js/first.js",
"pipeline/js/second.js",
"pipeline/js/application.js",
"pipeline/templates/**/*.jst",
),
"output_filename": "scripts_sha384.js",
"integrity": "sha384",
},
"scripts_sri_sha512": {
"source_filenames": (
"pipeline/js/first.js",
"pipeline/js/second.js",
"pipeline/js/application.js",
"pipeline/templates/**/*.jst",
),
"output_filename": "scripts_sha512.js",
"integrity": "sha512",
},
},
}

Expand Down
Loading

0 comments on commit 9fd17d6

Please sign in to comment.