mirror of
https://github.com/bigskysoftware/htmx.git
synced 2026-03-13 18:08:10 +00:00
security improvements
- add the `htmx.config.selfRequestsOnly` option - add the `htmx:validateUrl` event - better security documentation (incomplete, need to finish CORS)
This commit is contained in:
parent
61945431aa
commit
19cb15caef
18
src/htmx.js
18
src/htmx.js
@ -73,6 +73,7 @@ return (function () {
|
||||
getCacheBusterParam: false,
|
||||
globalViewTransitions: false,
|
||||
methodsThatUseUrlParams: ["get"],
|
||||
selfRequestsOnly: false
|
||||
},
|
||||
parseInterval:parseInterval,
|
||||
_:internalEval,
|
||||
@ -2844,6 +2845,18 @@ return (function () {
|
||||
return arr;
|
||||
}
|
||||
|
||||
function verifyPath(elt, path, requestConfig) {
|
||||
var url = new URL(path, document.location.href);
|
||||
var hostname = document.location.hostname;
|
||||
var sameHost = hostname !== url.hostname;
|
||||
if (htmx.config.selfRequestsOnly) {
|
||||
if (sameHost) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return triggerEvent(elt, "htmx:validateUrl", mergeObjects({url: url, sameHost: sameHost}, requestConfig));
|
||||
}
|
||||
|
||||
function issueAjaxRequest(verb, path, elt, event, etc, confirmed) {
|
||||
var resolve = null;
|
||||
var reject = null;
|
||||
@ -3072,6 +3085,11 @@ return (function () {
|
||||
}
|
||||
}
|
||||
|
||||
if (!verifyPath(elt, finalPath, requestConfig)) {
|
||||
triggerErrorEvent(elt, 'htmx:invalidPath', requestConfig)
|
||||
return;
|
||||
};
|
||||
|
||||
xhr.open(verb.toUpperCase(), finalPath, true);
|
||||
xhr.overrideMimeType("text/html");
|
||||
xhr.withCredentials = requestConfig.withCredentials;
|
||||
|
||||
@ -6,7 +6,6 @@ describe("security options", function() {
|
||||
});
|
||||
afterEach(function() {
|
||||
this.server.restore();
|
||||
clearWorkArea();
|
||||
});
|
||||
|
||||
it("can disable a single elt", function(){
|
||||
@ -105,4 +104,50 @@ describe("security options", function() {
|
||||
this.server.respond();
|
||||
btn.innerHTML.should.equal("Clicked a second time");
|
||||
})
|
||||
|
||||
it("can make egress cross site requests when htmx.config.selfRequestsOnly is enabled", function(done){
|
||||
htmx.logAll()
|
||||
// should trigger send error, rather than reject
|
||||
var listener = htmx.on("htmx:sendError", function (){
|
||||
htmx.off("htmx:sendError", listener);
|
||||
done();
|
||||
});
|
||||
this.server.restore(); // use real xhrs
|
||||
// will 404, but should respond
|
||||
var btn = make('<button hx-get="https://hypermedia.systems/www/test">Initial</button>')
|
||||
btn.click();
|
||||
})
|
||||
|
||||
it("can't make egress cross site requests when htmx.config.selfRequestsOnly is enabled", function(done){
|
||||
htmx.logAll()
|
||||
// should trigger send error, rather than reject
|
||||
htmx.config.selfRequestsOnly = true;
|
||||
var listener = htmx.on("htmx:invalidPath", function (){
|
||||
htmx.config.selfRequestsOnly = false;
|
||||
htmx.off("htmx:invalidPath", listener);
|
||||
done();
|
||||
})
|
||||
this.server.restore(); // use real xhrs
|
||||
// will 404, but should respond
|
||||
var btn = make('<button hx-get="https://hypermedia.systems/www/test">Initial</button>')
|
||||
btn.click();
|
||||
})
|
||||
|
||||
it("can cancel egress request based on htmx:validateUrl event", function(done){
|
||||
htmx.logAll()
|
||||
// should trigger send error, rather than reject
|
||||
var pathVerifier = htmx.on("htmx:validateUrl", function (evt){
|
||||
evt.preventDefault();
|
||||
htmx.off("htmx:validateUrl", pathVerifier);
|
||||
})
|
||||
var listener = htmx.on("htmx:invalidPath", function (){
|
||||
htmx.config.selfRequestsOnly = false;
|
||||
htmx.off("htmx:invalidPath", listener);
|
||||
done();
|
||||
})
|
||||
this.server.restore(); // use real xhrs
|
||||
// will 404, but should respond
|
||||
var btn = make('<button hx-get="https://hypermedia.systems/www/test">Initial</button>')
|
||||
btn.click();
|
||||
})
|
||||
});
|
||||
@ -1462,30 +1462,101 @@ to generate a different `ETag` for each content.
|
||||
|
||||
## Security
|
||||
|
||||
htmx allows you to define logic directly in your DOM. This has a number of advantages, the
|
||||
largest being [Locality of Behavior](@/essays/locality-of-behaviour.md) making your system
|
||||
more coherent.
|
||||
htmx allows you to define logic directly in your DOM. This has a number of advantages, the largest being
|
||||
[Locality of Behavior](@/essays/locality-of-behaviour.md), making your system more coherent.
|
||||
|
||||
One concern with this approach, however, is security. This is especially the case if you are injecting user-created
|
||||
content into your site without any sort of HTML escaping discipline.
|
||||
One concern with this approach, however, is security: since htmx increases the expressiveness of HTML, if a malicious
|
||||
user is able to inject HTML into your application they can leverage this expressiveness.
|
||||
|
||||
You should, of course, escape all 3rd party untrusted content that is injected into your site to prevent, among other issues, [XSS attacks](https://en.wikipedia.org/wiki/Cross-site_scripting). Attributes starting with `hx-` and `data-hx`, as well as inline `<script>` tags should be filtered.
|
||||
### Rule 1: Escape All User Content
|
||||
|
||||
It is important to understand that htmx does *not* require inline scripts or `eval()` for most of its features. You (or your security team) may use a [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) that intentionally disallows inline scripts and the use of `eval()`. This, however, will have *no effect* on htmx functionality, which will still be able to execute JavaScript code placed in htmx attributes and may be a security concern. With that said, if your site relies on inline scripts that you do wish to allow and have a CSP in place, you may need to define [htmx.config.inlineScriptNonce](#config)--however, HTMX will add this nonce to *all* inline script tags it encounters, meaning a nonce-based CSP will no longer be effective for HTMX-loaded content.
|
||||
The first rule of HTML-based web development has always been: *do not trust input from the user*. You should escape all
|
||||
3rd party, untrusted content that is injected into your site. This is to prevent, among other issues,
|
||||
[XSS attacks](https://en.wikipedia.org/wiki/Cross-site_scripting).
|
||||
|
||||
To address this, if you don't want a particular part of the DOM to allow for htmx functionality, you can place the
|
||||
`hx-disable` or `data-hx-disable` attribute on the enclosing element of that area.
|
||||
The good news is that this is a very old and well known rule, and the vast majority of server-side templating languages
|
||||
support [automatic escaping](https://docs.djangoproject.com/en/4.2/ref/templates/language/#automatic-html-escaping) of
|
||||
content.
|
||||
|
||||
This will prevent htmx from executing within that area in the DOM:
|
||||
With that being said, there are times people choose to inject HTML more dangerously, often via some sort of `raw()`
|
||||
mechanism in their templating language. This can be done for good reasons, but if the content being injected is coming
|
||||
from a 3rd party then it _must_ be scrubbed, including removing attributes starting with `hx-` and `data-hx`, as well as
|
||||
inline `<script>` tags, etc.
|
||||
|
||||
If you are injecting raw HTML and doing your own escaping, a best practice is to *whitelist* the attributes and tags you
|
||||
allow, rather than to blacklist the ones you disallow.
|
||||
|
||||
### htmx Security Tools
|
||||
|
||||
Of course, bugs happen and developers are not perfect, so it is good to have a layered approach to security for
|
||||
your web application. Browsers have a built-in protection layer, [Content Security Policies](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP),
|
||||
which we will discuss [below](#csp-options). But htmx provides tools to help secure your application as well.
|
||||
|
||||
Let's take a look at them.
|
||||
|
||||
#### `hx-disable`
|
||||
|
||||
The first tool htmx provides to help further secure your application is the [`hx-disable`](/attributes/hx-disable)
|
||||
attribute. This attribute will prevent processing of all htmx attributes on a given element, and on all elements within
|
||||
it. So, for example, if you were including raw HTML content in a template (again, not recommended!) then you could place
|
||||
a div around the content with the `hx-disable` attribute on it:
|
||||
|
||||
```html
|
||||
<div hx-disable>
|
||||
<%= user_content %>
|
||||
<%= raw(user_content) %>
|
||||
</div>
|
||||
```
|
||||
|
||||
This approach allows you to enjoy the benefits of [Locality of Behavior](@/essays/locality-of-behaviour.md)
|
||||
while still providing additional safety if your HTML-escaping discipline fails.
|
||||
And htmx will not process any htmx-related attributes or features found in that content.
|
||||
|
||||
#### `hx-history`
|
||||
|
||||
Another security consideration is htmx history cache. You may have pages that have sensitive data that you do not
|
||||
want stored in the users `localStorage` cache. You can omit a given page from the history cache by including the
|
||||
[`hx-history`](/attributes/hx-history) attribute anywhere on the page, and setting its value to `false`.
|
||||
|
||||
#### Configuration Options
|
||||
|
||||
htmx also provides configuration options related to security:
|
||||
|
||||
* `htmx.config.selfRequestsOnly` - if set to `true`, only requests to the same domain as the current document will be allowed
|
||||
* `htmx.config.historyCacheSize` - can be set to `0` to avoid storing any HTML in the `localStorage` cache
|
||||
* `htmx.config.allowEval` - can be set to `false` to disable all features of htmx that rely on eval:
|
||||
* event filters
|
||||
* `hx-on:` attributes
|
||||
* `hx-vals` with the `js:` prefix
|
||||
* `hx-headers` with the `js:` prefix
|
||||
|
||||
Note that all features removed by disabling `eval()` can be reimplemented using your own custom javascript and the
|
||||
htmx event model.
|
||||
|
||||
#### Events
|
||||
|
||||
If you want to allow requests to some domains beyond the current host, but not leave things totally open, you can
|
||||
use the `htmx:validateUrl` event. This event will have the request URL available in the `detail.url` slot, as well
|
||||
as a `sameHost` property.
|
||||
|
||||
You can inspect these values and, if the request is not valid, invoke `preventDefault()` on the event to prevent the
|
||||
request from being issued.
|
||||
|
||||
```javascript
|
||||
document.body.addEventListener('htmx:validateUrl', function (evt) {
|
||||
// only allow requests to the current server as well as myserver.com
|
||||
if (!evt.detail.sameHost && evt.detail.url.hostname !== "myserver.com") {
|
||||
evt.preventDefault();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### CSP Options
|
||||
|
||||
Browsers provide excellent tools for further securing your web application.
|
||||
|
||||
//TODO buff out relevant CSP options
|
||||
|
||||
```html
|
||||
<meta http-equiv="Content-Security-Policy" content="connect-src 'self'; default-src https;">
|
||||
```
|
||||
|
||||
## Configuring htmx {#config}
|
||||
|
||||
@ -1494,30 +1565,31 @@ listed below:
|
||||
|
||||
<div class="info-table">
|
||||
|
||||
| Config Variable | Info |
|
||||
|--------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `htmx.config.historyEnabled` | defaults to `true`, really only useful for testing |
|
||||
| `htmx.config.historyCacheSize` | defaults to 10 |
|
||||
| `htmx.config.refreshOnHistoryMiss` | defaults to `false`, if set to `true` htmx will issue a full page refresh on history misses rather than use an AJAX request |
|
||||
| `htmx.config.defaultSwapStyle` | defaults to `innerHTML` |
|
||||
| `htmx.config.defaultSwapDelay` | defaults to 0 |
|
||||
| `htmx.config.defaultSettleDelay` | defaults to 20 |
|
||||
| `htmx.config.includeIndicatorStyles` | defaults to `true` (determines if the indicator styles are loaded) |
|
||||
| `htmx.config.indicatorClass` | defaults to `htmx-indicator` |
|
||||
| `htmx.config.requestClass` | defaults to `htmx-request` |
|
||||
| `htmx.config.addedClass` | defaults to `htmx-added` |
|
||||
| `htmx.config.settlingClass` | defaults to `htmx-settling` |
|
||||
| `htmx.config.swappingClass` | defaults to `htmx-swapping` |
|
||||
| `htmx.config.allowEval` | defaults to `true` |
|
||||
| `htmx.config.inlineScriptNonce` | defaults to `''`, meaning that no nonce will be added to inline scripts |
|
||||
| `htmx.config.useTemplateFragments` | defaults to `false`, HTML template tags for parsing content from the server (not IE11 compatible!) |
|
||||
| `htmx.config.wsReconnectDelay` | defaults to `full-jitter` |
|
||||
| `htmx.config.disableSelector` | defaults to `[disable-htmx], [data-disable-htmx]`, htmx will not process elements with this attribute on it or a parent |
|
||||
| `htmx.config.timeout` | defaults to 0 in milliseconds |
|
||||
| `htmx.config.defaultFocusScroll` | if the focused element should be scrolled into view, defaults to false and can be overridden using the [focus-scroll](@/attributes/hx-swap.md#focus-scroll) swap modifier. |
|
||||
| `htmx.config.getCacheBusterParam` | defaults to false, if set to true htmx will include a cache-busting parameter in `GET` requests to avoid caching partial responses by the browser |
|
||||
| `htmx.config.globalViewTransitions` | if set to `true`, htmx will use the [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) API when swapping in new content. |
|
||||
| `htmx.config.methodsThatUseUrlParams` | defaults to `["get"]`, htmx will format requests with this method by encoding their parameters in the URL, not the request body |
|
||||
| Config Variable | Info |
|
||||
|---------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `htmx.config.historyEnabled` | defaults to `true`, really only useful for testing |
|
||||
| `htmx.config.historyCacheSize` | defaults to 10 |
|
||||
| `htmx.config.refreshOnHistoryMiss` | defaults to `false`, if set to `true` htmx will issue a full page refresh on history misses rather than use an AJAX request |
|
||||
| `htmx.config.defaultSwapStyle` | defaults to `innerHTML` |
|
||||
| `htmx.config.defaultSwapDelay` | defaults to 0 |
|
||||
| `htmx.config.defaultSettleDelay` | defaults to 20 |
|
||||
| `htmx.config.includeIndicatorStyles` | defaults to `true` (determines if the indicator styles are loaded) |
|
||||
| `htmx.config.indicatorClass` | defaults to `htmx-indicator` |
|
||||
| `htmx.config.requestClass` | defaults to `htmx-request` |
|
||||
| `htmx.config.addedClass` | defaults to `htmx-added` |
|
||||
| `htmx.config.settlingClass` | defaults to `htmx-settling` |
|
||||
| `htmx.config.swappingClass` | defaults to `htmx-swapping` |
|
||||
| `htmx.config.allowEval` | defaults to `true` |
|
||||
| `htmx.config.inlineScriptNonce` | defaults to `''`, meaning that no nonce will be added to inline scripts |
|
||||
| `htmx.config.useTemplateFragments` | defaults to `false`, HTML template tags for parsing content from the server (not IE11 compatible!) |
|
||||
| `htmx.config.wsReconnectDelay` | defaults to `full-jitter` |
|
||||
| `htmx.config.disableSelector` | defaults to `[disable-htmx], [data-disable-htmx]`, htmx will not process elements with this attribute on it or a parent |
|
||||
| `htmx.config.timeout` | defaults to 0 in milliseconds |
|
||||
| `htmx.config.defaultFocusScroll` | if the focused element should be scrolled into view, defaults to false and can be overridden using the [focus-scroll](@/attributes/hx-swap.md#focus-scroll) swap modifier. |
|
||||
| `htmx.config.getCacheBusterParam` | defaults to false, if set to true htmx will include a cache-busting parameter in `GET` requests to avoid caching partial responses by the browser |
|
||||
| `htmx.config.globalViewTransitions` | if set to `true`, htmx will use the [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) API when swapping in new content. |
|
||||
| `htmx.config.methodsThatUseUrlParams` | defaults to `["get"]`, htmx will format requests with this method by encoding their parameters in the URL, not the request body |
|
||||
| `htmx.config.selfRequestsOnly` | defaults to `false`, if set to `true` will only allow AJAX requests to the same domain as the current document |
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@ -419,6 +419,26 @@ granular events available, like [`htmx:beforeRequest`](#htmx:beforeRequest) or [
|
||||
|
||||
* `detail.elt` - the element that triggered the request
|
||||
|
||||
### Event - `htmx:validateUrl` {#htmx:validateUrl}
|
||||
|
||||
This event is triggered before a request is made, allowing you to validate the URL that htmx is going to request. If
|
||||
`preventDefault()` is invoked on the event, the request will not be made.
|
||||
|
||||
```javascript
|
||||
document.body.addEventListener('htmx:validateUrl', function (evt) {
|
||||
// only allow requests to the current server as well as myserver.com
|
||||
if (!evt.detail.sameHost && evt.detail.url.hostname !== "myserver.com") {
|
||||
evt.preventDefault();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
##### Details
|
||||
|
||||
* `detail.elt` - the element that triggered the request
|
||||
* `detail.url` - the URL Object representing the URL that a request will be sent to.
|
||||
* `detail.sameHost` - will be `true` if the request is to the same host as the document
|
||||
|
||||
### Event - `htmx:validation:validate` {#htmx:validation:validate}
|
||||
|
||||
This event is triggered before an element is validated. It can be used with the `elt.setCustomValidity()` method
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user