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:
Carson Gross 2023-07-31 11:31:42 -06:00
parent 61945431aa
commit 19cb15caef
4 changed files with 193 additions and 38 deletions

View File

@ -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;

View File

@ -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();
})
});

View File

@ -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>

View File

@ -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