TL;DR
Pentesting web applications that use Blazor server comes with unique challenges, especially without tooling. In this post, we discuss why such challenges exist and provide a Burp Suite Extension to address them.
Introduction
During a web application assessment, we encountered ASP.NET’s “Blazor” server for the first time. After attempting a few basic test cases, we realized this was not like any app we’ve tested before. For starters, all the messages transmitted by the application included seemingly random binary characters. You can make out some strings within these messages, but most of the data is not easily readable (Figure 1).

Additionally, any attempts we made to tamper with or replay requests resulted in an error and the connection being renegotiated (Figure 2).

These initial observations proved to be major hindrances for testing a Blazor server application: limited readability and the inability to tamper with data.
Blazor Basics
Before we dive into addressing these obstacles, we first need to cover some of the basics. Blazor is a framework that comes with a variety of hosting options: WebAssembly (WASM), Server-Side ASP.NET, and Native Client. For the purposes of this blog post, we’ll focus on server-side Blazor, which integrates with SignalR to send browser events and receive pre-rendered page updates. By default, Blazor server applications communicate via WebSockets, though other transports such as Long Polling over HTTP are available as well. Since the end goal here is a Burp Suite extension, we’ll need to use HTTP as the Burp Suite extender APIs previously had little to no WebSockets support.
Note: Since this project began, Portswigger has released newer versions of the Montoya APIs which offer better extension support for WS. Handling Blazor messages over WS is currently under development for the BlazorTrafficProcessor extension.
Forcing a Blazor application to use Long Polling is possible within the negotiation process. When a web browser connects to a Blazor server, the first request is a version negotiation. This specifies the version and transports that will be used by the application.
An example request would look like:
POST /_blazor/negotiate?negotiateVersion=1 HTTP/1.1
Content-Length: 0
[...]
The response is as follows:
HTTP/1.1 200 OK
Content-Length: 316
Content-Type: application/json
[...]
{"negotiateVersion":1,"connectionId":"******","connectionToken":"******","availableTransports":[{"transport":"WebSockets","transferFormats":["Text","Binary"]},{"transport":"ServerSentEvents","transferFormats":["Text"]},{"transport":"LongPolling","transferFormats":["Text","Binary"]}]}
Using a Burp match and replace rule, you can remove the WebSockets
transport from the response, forcing the browser to fall back to use Long Polling over HTTP. The BlazorTrafficProcessor extension will automatically downgrade any Blazor connections from WS to Long Polling.
After modifying the response to exclude WebSockets
, the browser console will show a warning to indicate a WebSocket connection failed and Long Polling will be used instead.
Warning: Failed to connect via WebSockets, using the Long Polling fallback transport. This may be due to a VPN or proxy blocking the connection. To trouble shoot this, visit
https://aka.ms/blazor-server-using-fallback-long-polling
.
By forcing the application to use Long Polling, all Blazor traffic will now occur over HTTP which makes Blazor data more accessible to prospective Burp Suite extensions. However, we still don’t know how the Blazor messages are formatted.
We know that Blazor applications expect messages to be in a particular format and order, with any deviations resulting in an error. If we turn to the documentation, Microsoft identifies this format as MessagePack and outlines how it can be used in ASP.NET applications.
MessagePack
MessagePack is another serialization format used to package structured data, like JSON, XML, etc. The key difference with MessagePack is that it is binary in nature, meaning specific bytes are used to indicate the types and length of serialized data.
While Blazor server uses MessagePack, the traffic is specifically formatted according to Blazor’s own Hub Protocol specification. Therefore, generic MessagePack parsers like the Burp Suite MessagePack extension available from the BApp Store will not work with Blazor traffic. Take the following BlazorPack message for example.

If we use the MessagePack extension on this message, the result is just 25
and the rest of the message is ignored. This is because \x00 - \x7f
represent positive integers in the MessagePack specification. The extension sees the first \x19
, converts it to the decimal value of 25
, and fails to parse the rest of the message. We’ll need a customized MessagePack parser to properly read these messages.
Blazor messages are formatted according to the Hub Protocol specification.
(
[Length]
[Body]
)([Length]
[Body]
)...
Length
is a variable-size integer representing the size of Body
, which is the actual message. A variable-size integer is one that can occupy a varying number of bytes, depending on the integer’s value. For example, a small integer may occupy a single byte whereas a larger integer may occupy up to five bytes. This value is crucial for parsing messages accurately and is one of the reasons that our tampering attempts failed. If you modify a string to be a different length than the original message, you’d need to update the Length
variable-size integer as well. Manually performing these calculations for every request you want to tamper with is tedious and inefficient.
For the Body
field, there are different types of messages supported by Blazor (i.e., Invocation
, StreamInvocation
, Ping
, etc.). However, while proxying traffic and testing a sample Blazor application, we rarely saw any other types of messages being used other than Invocation
. This is where we’ll focus for an example message breakdown.
InvocationMessage Analysis
The predominant message type observed while testing Blazor applications is an InvocationMessage
, used to render page updates and submit data.
Taking a look at the specification, we see that there is the following structure:
[
1
, Headers
, InvocationId
, Target
, [Arguments]
, [StreamIds]
]
-
1
– the message type,InvocationMessage
types are 1.Headers
– a map containing string key-value pairs. During testing of a sample Blazor app, this was observed to only be equal to a null/empty map.InvocationId
– this can either be NIL to indicate a lack of an invocation ID or a string that holds the value. Again, this was always NIL during testing.Target
– a string representing the backend function to call.Arguments
– an array of arguments to pass to that backend function.StreamIds
– an array of strings representing unique stream identifiers. Again, this was always NIL or non-existent in the messages observed whilst testing.
InvocationMessage Example and Byte Breakdown
The following request body was sent after modifying a text input field to be foobar
:
®•À·BeginInvokeDotNetFromJS•¡1À²DispatchEventAsyncÙx[{"eventHandlerId":4,"eventName":"change","eventFieldInfo":{"componentId":20,"fieldValue":"foobar"}},{"value":"foobar"}]
Now, taking a look at the bytes:
\xae\x01
\x95
\x01
\x80
\xc0
\xb7
BeginInvokeDotNetFromJS
\x95
\xa1
1
\xc0
\xb2
DispatchEventAsync
\x01
\xd9\x78
[{"eventHandlerId":4,"eventName":"change","eventFieldInfo":{"componentId":20,"fieldValue":"foobar"}},{"value":"foobar"}]
-
\xae\x01
–174
Variable-sized integer corresponding to theLength
parameter for the Hub Protocol.\x95
–5
MessagePack array header representing the length of the wholeInvocationMessage
body (5).\x01
–1
First element in theInvocationMessage
array corresponding to the message type (InvocationMessage = 1).\x80
–0
Second element in theInvocationMessage
array indicating a map of length 0.\xc0
–NIL
Third element in theInvocationMessage
array corresponding to theInvocationID
(NIL)\xb7
–23
MessagePack header corresponding to the length of the fourth element in theInvocationMessage
(23 bytes).BeginInvokeDotNetFromJS
Fourth element in theInvocationMessage
array representing theTarget
value (BeginInvokeDotNetFromJS).\x95
–5
Fifth element in theInvocationMessage
array corresponding to MessagePack array header length (5 bytes).\xa1
–1
MessagePack array header representing the size in bytes for the first element in theArguments
array (1 byte).1
Raw string representing the value of the first element in theArguments
array (1).\xc0
–NIL
Second element in theArguments
array (NIL).\xb2
–18
MessagePack header corresponding to the length in bytes of the third element in theArguments
array (18 bytes).DispatchEventAsync
Raw string representing the value of the third element in theArguments
array (DispatchEventAsync).\x01
–1
MessagePack integer representing the value of the fourth element in theArguments
array (1).\xd9\x78
–120
MessagePack header corresponding to the length in bytes of the fifth element in theArguments
array (120 bytes).[{“eventHandlerId”:4,…
MessagePack raw string corresponding to the value of the fifth element in theArguments
array ([{“eventHandlerId”:4,…)- There are no
StreamIds
in this message.
Each string in a Blazor message is preceded by a MessagePack string header to indicate the size of the string. This adds another layer of complexity to manually tampering with messages; not only do you have to update the Length
variable-size integer, but you’d have to update the size headers for each tampered string as well.
BlazorTrafficProcessor (BTP) Burp Suite Extension
In summary, the two biggest obstacles when it comes to testing Blazor applications are:
- Readability: the serialized messages can be difficult to read by themselves. As shown above, this is due to bytes in MessagePack representing specific values or types, resulting in seemingly random data to the naked eye.
While some data may be easily readable (i.e., the JSON array above containing user input), any attempt to modify these values will require modification to the preceding size bytes as well. This brings us to our next obstacle:
- Tampering Difficulties: because the messages are serialized according to the Hub Protocol specification, tampering with a value without accounting for the respective size bytes will result in a malformed MessagePack stream that the server can’t read. When this happens, the Blazor application will often error out and renegotiate your connection.
The BlazorTrafficProcessor (BTP) Burp Suite extension addresses both of these issues by providing testers with the ability to convert request bodies from BlazorPack to JSON and vice versa. JSON is more common in web applications and is easier to read due to being text-based rather than binary-based. By providing both deserialization and serialization functionality, testers can leverage this extension to capture and deserialize a request, modify the desired parameter(s), then serialize back to BlazorPack and submit the request.
Demo
The initial version of the extension has two primary features. The first of which is a BTP Request/Response tab that appears on every in-scope BlazorPack message with the ability to convert BlazorPack to JSON. The second is a BTP Burp Suite tab that provides a sandbox for users to serialize/deserialize BlazorPack messages at-will. Any requests/responses that contain BlazorPack serialized data will be highlighted in the HTTP History tab as Cyan.
Reading
The BTP tab will appear on every in-scope request and response in your Burp history that is serialized using BlazorPack.

This tab simply displays the BlazorPack body converted into a JSON array of message objects.

Tampering
Within Blazor server applications, the JSInvokable
attribute allows developers to expose DotNet functions to the client-side JavaScript. Take the following snippet for example:
[JSInvokable("CallMe")] public static void hiddenFunc(String var) { Console.WriteLine("Hidden function called!"); Console.WriteLine(var); }
This is a simple example that just logs some user input to the console, but there are no web pages that allow the user to call this function. Instead, the DotNet.invokeMethodAsync
JavaScript function can be used (as outlined here).

After the function has been called from the browser, Burp captured the serialized message that was sent:

Sending the above payload to repeater and using the extension to deserialize yields the following:

The input of foo
is contained within a 1-element array, so let’s try tampering and replaying the request. For demo purposes, we’ll change the JSON payload in the BTP tab to include a newline to see if newlines get written to the console output:
[{"Target":"BeginInvokeDotNetFromJS","Headers":0,"Arguments":["3","BlazorServerTestApp","CallMe",0,["Line1\r\nLine2"]],"MessageType":1}]
Clicking on the Raw tab to serialize the data back into MessagePack yields:

Upon sending the request, the response is a simple 200 OK:
HTTP/1.1 200 OK
Content-Length: 0
Connection: close
Content-Type: text/plain
Server: Kestrel
Important note about Blazor server Long Polling: sending any type of invocation POST request will not return data in the response. Instead, Long Polling keeps an open GET request that receives server updates as they become available. As such, if you send a request with malformed input that causes an error, you won’t see that error unless you look at subsequent GET requests.
Checking the console log and we see that the payload did in fact create two distinct lines with our input:

Ad-Hoc Serialization & Deserialization
The BTP extension also includes a new tab added to Burp Suite, aptly named BTP. While you can copy and paste data into this tab, it is recommended to use the “Send body to BTP” right-click menu option (Figure 11) to ensure the whole message is copied appropriately. This feature applies to both requests and responses.

The message will then appear in the BTP tab as shown below:

This tab can be used for ad-hoc Blazor conversions with the following steps:
- Select the conversion type (JSON->Blazor or Blazor-JSON)
- Enter the message in the left-hand editor
- Click Serialize/Deserialize Button
- Results are shown in the right-hand editor
What are these “Target” values I keep seeing?
While testing Blazor server applications, you’re likely to run into various Target
values such as: BeginInvokeDotNetFromJS
, OnRenderCompleted
, OnLocationChanged
, etc. These functions themselves are not specific to the app that you’re testing. Rather, they are built-in to ASP.NET Core as part of ComponentHub
and are used to facilitate communications between the frontend JavaScript and the backend .NET application. For example, OnLocationChanged
is used to navigate between pages in the web application. The implementation of these functions can be found in the “aspnetcore” repository on GitHub here.
It is important to distinguish between what’s native and what’s custom in Blazor server applications, since your goal will likely be to find vulnerabilities in the app that you’re testing, not in Blazor itself. As such, focus your testing efforts on fields that contain your input (i.e., arguments to BeginInvokeDotNetFromJS
) as opposed to normal Blazor operations (i.e., OnRenderCompleted
or EndInvokeJSFromDotNet
). With that said, security researchers can also utilize the BTP extension to test Blazor server itself since tampering inputs is now easier.
The following screenshot (Figure 13) displays a deserialized request that updates the value of a text box in the application to be asdf
. The highlighted values are potential areas for tampering. The rest of the arguments are native to Blazor and tampering may cause a Blazor error, which would renegotiate your connection.

Consider a case where only client-side JavaScript is used to validate this input field. We could intercept the request, deserialize the message into JSON, enter a value that would otherwise fail validation, reserialize, and fire the request to bypass client-side input validation.
Further Research
Fuzzing
While this extension achieves the base proof-of-concept for a tool to process Blazor server traffic, a feature we believe would be valuable to testers is the ability to fuzz serialized inputs (similar to a Burp Suite intruder attack). However, application data is never returned in response to a POST request when using Long Polling. Instead, the client-side JavaScript polls the server with a GET request and passes an epoch timestamp. The response to these polling requests will contain app data and render updates, making it difficult to deduce which request caused an error or a specific response. For example, if you send a POST request that causes an error, the error would be returned to the next GET (“poll”) request. Sending multiple error-inducing requests could cause the application to return several errors in one response body or spread out across subsequent polling responses. With no clear way to map one input to one response/error, we decided it best to leave out fuzzing for the first iteration of this tool. We are still looking for effective ways to fuzz Blazor applications and a fuzzing feature is on the roadmap for the BlazorTrafficProcessor extension.
WebSockets
BTP is fully dependent on the ability to downgrade a Blazor server connection from WebSockets to Long Polling over HTTP. ASP.NET developers have considered removing Long Polling support altogether, which could render BTP unusable. Ultimately Long Polling was left in the framework due to “unforeseen effects” of removing it, though it remains possible that support will be discontinued in the future.
For BTP to be future proof, it will need WebSockets support eventually. However, Blazor messages can be distributed across different WebSocket frames with a max size of 4096 bytes per frame. In other words, a Blazor message could start in one WebSocket frame and end in another. With limited support for WebSockets in Burp Suite’s Montoya APIs, it was not plausible to include Blazor WebSocket parsing for the initial version of BTP. However, this feature is currently under development as there are new iterations of Burp Suite’s Montoya APIs being released frequently.
You can find the latest version of the extension, as well as setup and usage instructions at our GitHub Repository here: https://github.com/AonCyberLabs/BlazorTrafficProcessor/releases.
Thank you for reading and happy Blazor hunting!
Author: Will Rabb
July 20, 2023
©Aon plc 2023