There are some good points here, but I think the author neglects to dig deeper and explain how you can mitigate some of these risks with htmx settings.
I've been using htmx with CSP for the past couple of weeks, and I do find that it expands attack surface in an unpleasant way, but I do still think it brings enough value that I'm willing to rely on other protections.
>That cors.php file contains the JavaScript payload, and also sets CORS headers so that the browser has access to it.
>HTMX has functionality that automatically adds the correct nonce to inline scripts it retrieves. This is convenient, but totally breaks the security model of CSP with nonces.
Agreed, this seems like an anti-feature that completely subverts CSP.
>Of course, this is trivial to bypass: just close the div tag with </div> and insert your payload outside of the element with the hx-disable attribute.
This is true, but it kind of glosses over the fact that you have to screw up a lot more to give the attacker complete control over the HTML. Usually you're not just executing attacker-controlled content as HTML. If you're using a templating system, the more likely vulnerability is that the attacker is limited to escaping an HTML attribute and adding extra one. That said, it's true that if you have a vulnerability that allows an attacker can escape one context, it's much more likely that they can escape them all.
>For these to work, the application has to allow evaluating dynamic code, using the CSP option unsafe-eval. However, allowing unsafe-eval immediately makes it possible to inject JavaScript using HTMX functionality.
I think this is the weakest point, as htmx works well without unsave-eval. I've been using it in my app without unsafe-eval. The features you lose are just convenience features for writing HTML that you can still achieve by subscribing to htmx events in JS. Yes, it means you have to write a little bit more JS, but it's much better than including unsafe-eval.
I wish that htmx would be a bit more CSP-friendly, but it's much better than other similar frameworks.
In this case, htmx would still prevent the xss because hx-disable causes htmx to ignore the hx-delete attribute in the child element.
But if we assume the server fails to encode angle brackets too, then the attacker wins because they can terminate the hx-disable, as described in the post.
But they're different levels of screwup. Accidentally letting the attacker control the value of the attribute is one level, letting them inject extra attributes is another level, and letting them inject extra HTML elements is another level.
Granted, if you're screwing up output encoding, you're likely going to grant the attacker the ability to inject elements at the same time that you allow them to inject attributes, but the defender has a bit more protection if they disallow or encode angle brackets in user input.
> letting them inject extra attributes is another level, and letting them inject extra HTML elements is another level.
These are ultimately the same problem. "Letting" is the wrong word here—nobody intends to let an XSS happen. These are two scenarios I've encountered in my own work:
- You're doing custom rendering, like markdown, and the output isn't exhaustively sanitized
- You used triple braces in Handlebars (or the unescaped output syntax of your favorite templating language) and the code gets reused for something unrelated that shouldn't be using unescaped output.
"Screwing up output encoding" is an alarmingly common vulnerability. XSS is arguably one of the most (if not the most) common security vulnerability ever. That's not to say it's a problem with HTMX, but the CSP exists specifically for this reason and HTMX essentially acts as an interpreter that can run arbitrary instructions. It's definitionally a way to bypass the CSP in the face of an XSS where a traditional attack would have been otherwise blocked by the CSP.
> If you allow random users to write arbitrary unsanitized HTML into other users’ pages, you have already lost
Not really. The whole point of CSP is that, even if someone manages to inject arbitrary HTML into your page due to a bug or oversight, you can prevent injected scripts from running entirely, severely limiting what an attacker can do.
I'm surprised at the amount of comments on this post that seem to be completely clueless about CSP.
CSP is a backstop that—when configured properly and used alongside restraint in how you script—can minimize some of the worst consequences of injection.
It doesn’t wash your hands clean of the responsibility to restrict what kinds of content users can inject into served pages.
Besides whether it is a real security risk, this is useful information if you have to work with a security department. In particular, there are corporate environments where certain CSP settings would require approval or are simply not possible.
HTMX always assumes that the incoming HTML is properly sanitized. If it isn't, the application is already vulnerable.
HTMX triggers do in fact use JavaScript eval(), which will get blocked with a CSP that does not allow it. But you can use standard JS scripts to add events. The same goes for inline CSS.
I don't see the point of this article, it's just common sense. If you plan which parts of the DOM are replaced using HTMX, never trust user input. You'll be fine. I use Golang with HTMX and it's amazingly productive.
I think the point is that htmx is in no way special. It's the same old category of vulnerability. It's why we have execvpe. It's why we have mysql_real_escape_string_no_really_we_mean_it_this_time().
I remember a similar point being made about LLM output. Now, I'm anything but a LLM fanboy, but if you pipe unknown text into a system interface that's squarely on you.
I think the point being made that you're replying to is that it has nothing to do with htmx. It isn't htmx that's not playing well with content security policy.
it’s unclear what point the post is trying to make. The outlined behaviour is not specific to HTMx per-se. These are security considerations for all server rendered pages.
The “basic” golden rules:
- Only call routes you control
- Always use an auto-escaping template engine
- Only serve user-generated content inside HTML tags
- If you have authentication cookies, set them with Secure, HttpOnly, and SameSite=Lax
The server can, at best, set the correct CSP header. It’s a validation performed entirely in the browser. Even the best intended rendering from the server can’t prevent CSP violation attempts when the client is executing some kind of script. That’s why even frameworks like Vue and React need a correctly configured CSP.
Rewrite the inline styles/scripts at build times to separate files, generate the appropriate script/style elements, compute the appropriate CSP directive.
(the boilerplate can be amortized over many such handlers)
Or at least use 'unsafe-hashes' and compute the hashes for onclick handlers and the like and include those in the CSP.
Separation of style/scripts/content has been encouraged for a long time, I don't know why people are walking it back. But if they insist then templates could also be structured more like a multipart document where those aspects sit in separate parts of the document.
As long as you do any of those when the template renderers are compiled then only scripts whitelisted at buildtime will be executed and scripts injected by dynamic content will get blocked.
Thank you for clarifying, now I understand what you mean. I think we’re on the same page! I like separating assets as they can also have a very different lifecycle than the structure / content of a page.
> These are security considerations for all server rendered pages
Well - "server rendered" to my ears primarily means plain html generated on the server using Perl, Python, PHP, CGI or whatever. I presume you mean "server rendered with some client-side code to handle injecting into the current DOM.
Am I just too old-school or does using "server rendered" to mean fairly specific things seem strange?
You're correct. Server-rendered pages may also have some client-side interactivity using, for example, JavaScript/AJAX.... It doesn't need to be a fancy framework like htmx, but one needs to be mindful about user input and remote content either way. The same "techniques" for securing a web page remain in effect.
It doesn't matter how it's rendered or where it's rendered. If it lets user A put unescaped/filtered/sanitised data on user B's pages, it's a security issue, no matter how the page was rendered. It's one that can be mitigated in various ways, but it's important to take into account.
one great feature of htmx is how easy it is to understand and develop extensions for. I've just implemented this extension which allows safer handling of CSP nonce if this was required by an application. Hopefully this will get accepted as an official htmx extension.
My advice is if your not using inline script tags or the eval feature just keep it simple and disable allowScriptTags and allowEval and set a good CSP header. Also make sure you set a htmx-config meta tag in your page headers to set your config and protect from injected meta tags.
If you need inline scripts in a few places then be aware you need to choose a good templating or auto escaping engine on the backend to protect you and think about user inputs and be careful when using any raw escape override functions. If you have a sensitive application that needs regular external pen-testing then look at things like my safe-nonce extension that gives you another layer of protection to sell.
I was under the impression that most (if not all) browser JS frameworks need hilariously bad CSPs like this, particularly for dynamic styling. Am I mistaken?
I'm surprised that CSPs with unsafe-inline aren't treated with the same hate that table based layouts get. They are really bad.
I feel 2/3 of the comments here are missing the point.
CSP is a mitigation for XSS vulnerabilities. Yeah, if you always sanitize everything in the right way, you won't have XSS vulnerabilities.
CSP was born out of the recognition that people fail to properly sanitize everything, even if they know that this is what they should do. Because it's complicated, and there are so many corner-cases and different ways to have XSS. CSP is a mitigation on top, in case your "we sanitize everything in the right way" goes wrong.
Making the point that you don't need CSP if you don't have XSS vulnerabilities is like saying C is secure as long as you know know how to use it and don't write code with memory safety bugs.
So, I get that `raw` prevents htmx from being used. I get that `<script>` still works.
But I find it scary that if `user_content` is `</div><div>...` that is actually injected, as raw HTML. I would expect that the `<%= raw(user_content) %> only has access to contents of the div it is in itself, and nothing more. But instead I understand that the HTML is injected as text (?) and then re-parsed (?).
In Angular, for example, the template is parsed into a DOM tree, and then template variables are placed in the correct place. This makes injection really hard. In the above example, it would be impossible to break out of the div.
Other template engines just do a string search/replace, and this makes injection easy. Then it's indeed possible to break out of the div just by injecting </div>.
The example you quoted comes directly from the HTMX docs. They don't specify which template system is used, and I don't immediately recognize the syntax to limit it to a specific template system.
Okay this whole article is hilarious. I still don’t want to use HTMX but now I will happily not use it, rather than angrily not use it like I do with golang.
You should try HTMX and Golang together, it's an amazingly productive way to build internal tools. Our JS framework of choice is react, but the time it takes to configure a new project in our mono-repo I could've build the whole tool with HTMX and Go. This isn't to shit on react (or our setup) they are both excellent, it's simply to highlight just how productive you can be with those tools.
It's a "use responsible" sort of tech stack though, but for a lot of things... well... it's fine. We're talking about a world where the alternative might be giving some team excel access directly to a production DB. Which is infinitely stupid x 2, but it still happens if development moves too slow.
HTMX fits into the middle ground: websites that are too dynamic to be made with a SSG or CMS, but not dynamic enough to be a SPA.
I don’t work on enough things that fit into that category to justify learning an entire new framework. I’d rather just write React or even use plain JS.
HTMX obviously can’t be used to build a SPA, so even if it were incredible I would still have to keep my React skills up. I’d also worry that if I build something in HTMX and it increases in scope, I’d end up rewriting it in React or maintaining a bird’s nest. It’s easy for me to tell the difference between webthings that “need to be SSG’d” and “needs React”, if I insert HTMX in the middle then I’d be second guessing if I made the right choice.
As for Golang, again I’m just not the target market for it. It took them so long to decide to add generics. For me generics are a basic feature not a nice to have. I don’t agree with the goals of the language, so there’s no point in me learning it.
So, CSP is designed to help against the case where an XSS already happened. So, a lot of this seems like valid criticism, but some parts seem wrong. The "loading malicious fragments" part looks more like the motivating case for using CSP in the first place, rather than an implication of HTMX, but correct me if I'm wrong.
The "unsafe eval" and "nonces for inline scripts" sections seem like valid criticisms of how HTMX works currently. It seems both are possible to disable using the configuration, though (meaning you could remove unsafe-eval from your CSP and htmx would still work except the disabled features).
The "hx-disable" one seems like a badly designed security feature, frankly. BUT, the code that rendered the unsanitized HTML content was not htmx (looked like an erb template), so htmx shouldn't really be blamed for that one.
Loading malicious fragments:
This is in no way related to htmx and is the same for all web implementations because you don't go get possible script code from random untrusted domains you don't trust or control. Also if he had tried this with the current version of htmx (2.0) he would have not been able to do this as it now defaults to blocking hx requests to external sites and you can only use local relative paths now. So win to htmx on this point.
Unsafe eval:
Yeah unsafe eval looks bad but almost every other client side framework also has this same issue and it is very hard to turn this one off on any modern interactive or SPA like website today. Htmx has the option to just ignore the two features that need unsafe eval and implement these features with other more custom solutions unlike most other solutions so I have to chalk this up as a second win for htmx.
Disabling HTMX with hx-disable:
I think it fair that hx-disable is not a fool proof security feature and is there to stop silly things and provide some isolation for content to separate it from htmx. But you should probably not allow un-sanitized data in here and expect this to save you.
Nonces for inline scripts:
Yeah htmx has some support for nonces and they can protect from inline scripts inserted into the page somehow but they won't protect you from scripts returned from your trusted local server via hx requests by design. Another small win here.
Configuration meta tag:
This one is a fair point and the use of malicious meta tags is an interesting attack vector. This may weaken some of the security features built into htmx but if you allow un-sanitized user input to be returned by your server like in his example app there are not many solutions that will come off any better off.
Conclusion:
When a site uses HTMX, the attack surface of HTML injection is the same as any other SSR solution. It is possible to limit the risk of XSS by using a content security policy. Security is simple and old school as you just sanitize your inputs which all modern backend stacks now make easy. But it not possible to have all HTMX features and provide 100% security against injection (unless you sanitize all inputs and only link to your own server).
This article seems to be full of security vulns that the hypothetical htmx user is deliberately injecting into their own page. Sure, if you deliberately make your app vulnerable, it's vulnerable. This isn't, uh, unique to htmx.
For example, sure you can allow htmx on your page to make requests to domains you don't control. This is the proverbial shooting yourself in the foot. If you are allowing this in your site, you have much deeper problems than htmx and need much bigger interventions than CSP.
The relevant thing here is that before, CSP would protect you even if you failed to sanitize an attacker-controlled input. Bringing in htmx allows the attacker to bypass CSP, and that hasn't been documented well before.
The htmx docs mention CSP in passing but don't explore the nuance that OP does.
I think that's only true if your CSP has the word 'unsafe' or '*' in it. If it doesn't, it shouldn't allow inline code or things from domains that you didn't whitelist.
If you have CSP enabled with standard settings, the attack won't execute. CSP saves you even if you screwed up that badly.
The problem is that even if you keep the CSP settings secure and you add htmx into your app, the attacker can effectively achieve XSS because htmx's functionality is powerful enough that it's mostly equivalent to having arbitrary JS execution:
The headers say "Server: AmazonS3" which means it's a static site. Given that it's also a personal blog, there's not much point in CSP headers because a) it's not processing user input and b) even if an attacker achieves XSS, there's no data to steal or useful malicious actions to take on a static personal blog.
Yeah the risk here is very low unless they start hosting more complicated content later on. But it is not hosted on S3 and is actually hosted by AWS CloudFront with a S3 origin which now has a built in feature to set security headers for your static site in only a few clicks so you can get that sweet sweet A rating ;)
I've been using htmx with CSP for the past couple of weeks, and I do find that it expands attack surface in an unpleasant way, but I do still think it brings enough value that I'm willing to rely on other protections.
>That cors.php file contains the JavaScript payload, and also sets CORS headers so that the browser has access to it.
The following htmx settings should defeat this:
>HTMX has functionality that automatically adds the correct nonce to inline scripts it retrieves. This is convenient, but totally breaks the security model of CSP with nonces.Agreed, this seems like an anti-feature that completely subverts CSP.
>Of course, this is trivial to bypass: just close the div tag with </div> and insert your payload outside of the element with the hx-disable attribute.
This is true, but it kind of glosses over the fact that you have to screw up a lot more to give the attacker complete control over the HTML. Usually you're not just executing attacker-controlled content as HTML. If you're using a templating system, the more likely vulnerability is that the attacker is limited to escaping an HTML attribute and adding extra one. That said, it's true that if you have a vulnerability that allows an attacker can escape one context, it's much more likely that they can escape them all.
>For these to work, the application has to allow evaluating dynamic code, using the CSP option unsafe-eval. However, allowing unsafe-eval immediately makes it possible to inject JavaScript using HTMX functionality.
I think this is the weakest point, as htmx works well without unsave-eval. I've been using it in my app without unsafe-eval. The features you lose are just convenience features for writing HTML that you can still achieve by subscribing to htmx events in JS. Yes, it means you have to write a little bit more JS, but it's much better than including unsafe-eval.
I wish that htmx would be a bit more CSP-friendly, but it's much better than other similar frameworks.