Skip to content

HTMX CSP extension (hx-csp) returns nonce mismatch, in contradiction with documentation #3840

@dblanque

Description

@dblanque

Hello!

I am having an issue with HTMX 4 (Beta 4) where hx-csp does not seem to correctly use the fresh nonce from the new partial response from the server, and get instead the following error:
hx-csp.js:90 htmx: [hx-csp] blocked <form#form-login> — nonce mismatch (possible injection)

Image

CSP Settings

SECURE_CSP = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF, CSP.NONCE, "'strict-dynamic'" ],
    "img-src": [CSP.SELF, "data:"],
}

Partial Snippet

<c-form
  x-data="form_login_data"
  id="form-login"
  class="self-center gap-0"
  hx-disable:inherited="this, #form-login-submit, #username, #email, #password, #select-language, #select-theme"
  @input="updateFormDisabledState"
  hx-nonce="{{ csp_nonce }}"
  hx-post="{% url "fe-login" %}"
  hx-swap="none"
  hx-headers='{"x-csrftoken": "{{ csrf_token }}"}'
  > (...)
</c-form>

I don't know if I'm being a dummy, or missing something but, according to the HTMX CSP Documentation which I quote:

The extension rewrites the response nonce to the page nonce so swapped-in elements pass subsequent nonce checks.
Before doing that rewrite, it scrubs any element that already carries the page nonce value from the raw response text.

In theory (and from what I can see in the response headers) each server response has a different nonce, just as the documentation says:

The server cannot know the page nonce — it only knows its own per-response nonce. So if the page nonce appears in a response, it was put there by an attacker, not the server. Scrubbing it first means the rewrite pass cannot accidentally promote attacker-controlled elements to trusted status.

I have also tried returning partials without hx-nonce when the request is an htmx request, however that results in a no-nonce error, which makes sense and is probably what should happen.

From what I gather, HTMX wanting to match the previous nonce to the next/fresh one isn't intended behavior, right? Or am I doing something wrong?

Regards and thanks in advance,
Dylan

Edit:

I also see in the hx-csp code that the following function compares the nonce to the page Nonce rather than the response Nonce...

function checkNonce(elt) {
    if (!pageNonce) return false;
    let eltNonce = getNonce(elt);
    if (eltNonce !== pageNonce && stripHxAttributes(elt, eltNonce)) return false;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions