Skip to content

[Detail Bug] Webpack loader: JS preload tags omit CSP nonce and SRI integrity attributes #432

@detail-app

Description

@detail-app

Summary

  • Context: The get_as_url_to_tag_dict function in webpack_loader/utils.py generates HTML tags for Webpack bundles, supporting both standard tags and <link rel="preload"> tags.
  • Bug: When generating preload tags for JavaScript bundles (is_preload=True), the function fails to include integrity and nonce attributes.
  • Actual vs. expected: For JavaScript preloads, it generates a basic <link> tag without security attributes, whereas for regular <script> tags and all CSS tags (both regular and preload), it correctly includes them.
  • Impact: Security policies like CSP and SRI are violated for preloaded JavaScript, which can cause the browser to block the preload or redundantly fetch the resource when the actual <script> tag is encountered.

Code with bug

        if chunk['name'].endswith(('.js', '.js.gz')):
            if is_preload:
                result[chunk['url']] = (
                    '<link rel="preload" as="script" href="{0}" {1}/>'  # <-- BUG 🔴 Missing {2} and {3} for integrity and nonce
                ).format(''.join([chunk['url'], suffix]), attrs)
            else:
                result[chunk['url']] = (
                    '<script src="{0}"{2}{3}{1}></script>'
                ).format(
                    ''.join([chunk['url'], suffix]),
                    attrs,
                    loader.get_integrity_attr(chunk, request, attrs_l),
                    loader.get_nonce_attr(chunk, request, attrs_l),
                )

Evidence

  1. Reproduction Test: A test case was created where a request with a CSP nonce and a configuration with integrity enabled were used.
    def test_js_preload_missing_integrity_and_nonce(self):
        with patch('webpack_loader.utils.get_loader') as mock_get_loader:
            mock_loader = mock_get_loader.return_value
            mock_loader.get_bundle.return_value = [
                {'name': 'main.js', 'url': '/static/main.js', 'integrity': 'sha256-js-integrity'}
            ]
            mock_loader.config = {'INTEGRITY': True, 'CSP_NONCE': True, 'CACHE': False}
            mock_loader.get_integrity_attr.return_value = ' integrity="sha256-js-integrity" '
            mock_loader.get_nonce_attr.return_value = 'nonce="test-nonce" '
            request = self.factory.get('/')
            request.csp_nonce = "test-nonce"

            tag_dict = get_as_url_to_tag_dict('main', request=request, is_preload=True, extension='js')
            tag = tag_dict['/static/main.js']
            self.assertIn('integrity="sha256-js-integrity"', tag)  # Fails here
            self.assertIn('nonce="test-nonce"', tag)             # Fails here
  1. Comparison with CSS: In the same file, CSS preloads are handled correctly by including all placeholders:
        elif chunk['name'].endswith(('.css', '.css.gz')):
            result[chunk['url']] = (
                '<link href="{0}" rel={2}{3}{4}{1}/>'
            ).format(
                ''.join([chunk['url'], suffix]),
                attrs,
                '"stylesheet"' if not is_preload else '"preload" as="style"',
                loader.get_integrity_attr(chunk, request, attrs_l),
                loader.get_nonce_attr(chunk, request, attrs_l),
            )

Why has this bug gone undetected?

Most users likely use render_bundle without the is_preload=True flag, or they do not have both SRI (INTEGRITY=True) and CSP (CSP_NONCE=True) configured. When preloading is used without these security features, the generated tag is valid HTML. Furthermore, if a browser ignores the preload due to missing integrity but later loads the script tag successfully, the developer might only notice a slight performance degradation (double fetch) rather than an outright failure, unless they check browser console warnings.

Recommended fix

Update the JavaScript preload block in get_as_url_to_tag_dict to include the integrity and nonce attributes, mirroring the logic used for regular script tags and CSS tags:

            if is_preload:
                result[chunk['url']] = (
                    '<link rel="preload" as="script" href="{0}"{2}{3} {1}/>' # <-- FIX 🟢
                ).format(
                    ''.join([chunk['url'], suffix]),
                    attrs,
                    loader.get_integrity_attr(chunk, request, attrs_l),       # <-- FIX 🟢
                    loader.get_nonce_attr(chunk, request, attrs_l),          # <-- FIX 🟢
                )

Related bugs

  • In the same file, _filter_by_extension uses a strict endswith check that excludes .js.gz and .css.gz files when filtering by js or css extensions, despite get_as_url_to_tag_dict having logic to support these compressed formats.
  • The use of OrderedDict[str, str]() as a constructor (line 78) causes a TypeError in Python 3.8, which is a supported version according to setup.py.

History

This bug was introduced in commit 755c67e (@hugobessa, 2024-07-28, PR #413). While adding support for CSP nonces and improving Subresource Integrity (SRI) handling, the change updated standard script tags and all CSS tags (including preloads) but neglected to apply the same security attributes to JavaScript preload tags.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions