-
Notifications
You must be signed in to change notification settings - Fork 6
Subscriber Security
Subscribers consume events generated by webhooks, by receiving a POSTed event to a public endpoint.
For example, this request could be POSTed to a subscriber endpoint:
POST http://mysubscriber/hello HTTP/1.1
Accept: application/json
User-Agent: ServiceStack .NET Client 4.56
Accept-Encoding: gzip,deflate
X-Webhook-Delivery: 7a6224aad9c8400fb0a70b8a71262400
X-Webhook-Event: hello
Content-Type: application/json
Host: mysubscriber
Content-Length: 26
Expect: 100-continue
Proxy-Connection: Keep-Alive
{
"Text": "I said hello"
}
The subscriber receives this event, and would normally return a '200 - OK', '201 - Created', '202 - Accepted' or '204 - No Content' response.
Since the endpoint is publicly available and listening, anyone could POST any data to it (once it becomes known on the internet), and therefore a malicious attacker could POST malicious events to the endpoint that may interrupt or interfere with subscriber's systems.
There are several strategies to not just anyone POST data to your endpoint. You could for example setup IP protection, or white-lists, or you could simply check a signature that is contained in the message. You could also do all of these things.
It is simple to verify the HMAC signature contained in the event [RFC3174]. (Described here on GitHub, and specified here).
HMAC signatures only ensure 'authenticity' that the sender of the message is in fact the expected sender of the message (i.e. the service that your registered your webhook with).
WARNING: HMAC signatures do not encrypt the message or provide any 'confidentiality'. The message remains in plain text, therefore your endpoints should always be secured with HTTPS.
The basic process for adding this protection is:
- Create, and store, a random secret key (8 or more characters) and then Base64 encode these characters.
- When registering a subscription, set the
config.secret
with this encoded secret. - When receiving a webhook event in your subscribers service, calculate the HMAC signature of the request body, (using the secret you stored), and compare that signature with the signature included in the
X-Hub-Signature
header of the webhook event itself. - If the signatures do not match, then disregard it, and don't process the request any further.
- Return a '401 - Unauthorized' if signatures do not match
As a normal secure pre-caution rotate your secrets often, and update your webhook subscription. Store your secrets safely.
Note: If you choose to return '401 - Unauthorized' (or in fact any 4XX statuscode) and the real caller was a system using the ServiceStack.Webhooks relay, the chances are that your webhook subscription will be automatically disabled. This should not concern you too much if things are setup correctly, because your signature check should not fail if the real caller was that service. But if for example, you have several webhooks pointing to the same endpoint, or you receive an event before you set up the secret, you may stop receiving events, because your webhook may have been automatically disabled.
Now, lets jump in and look at how you would verify that signature in your subscriber service, assuming that you have set a secret in your webhook subscription.
Assume you have exposed a ServiceStack service as your subscriber service to receive the event shown the top of this page.
It might look like this:
internal class MySubscriber : Service
{
public void Post(HelloDto request)
{
// They said hello!
var message = request.Text;
}
}
[Route("/hello", "POST")]
public class HelloDto
{
public string Text { get; set; }
}
Then this event is POSTed to it:
POST http://mysubscriber/hello HTTP/1.1
Accept: application/json
User-Agent: ServiceStack .NET Client 4.56
Accept-Encoding: gzip,deflate
X-Webhook-Delivery: 7a6224aad9c8400fb0a70b8a71262400
X-Hub-Signature: sha1=d1194597f3480b2efe8c4f970f817c5c7cbb98625ee941debdf619d42beb9aee
X-Webhook-Event: hello
Content-Type: application/json
Host: mysubscriber
Content-Length: 26
Expect: 100-continue
Proxy-Connection: Keep-Alive
{
"Text": "I said hello"
}
Notice that the header X-Hub-Signature
is now included in the request.
This header is only present if your subscription has defined a 'secret'. The event headers and body are the same as it would be if you didn't define a 'secret'. The only difference is the presence of the X-Hub-Signature
header.
Now, to ensure that the event was raised from the service that you subscribed to, you need to calculate a signature for the event using your stored secret, and compare that signature to the signature included in the X-Hub-Signature
header.
If they match, you have authenticated the sender of the event. Otherwise, if it does not match, or no signature is present then someone else might has sent the event, and you should ignore it.
You have a couple of options here for doing this verification in your subscriber service. Either do the verification manually in code or use the built-in authorization framework of ServiceStack (with the AuthFeature
) to do it for you.
First:
Install-Package ServiceStack.Webhooks.Interfaces
Then in your AppHost you need to register this special PreRequestFilter.
public override void Configure(Container container)
{
// We need this filter to allow us to read the request body in IRequest.GetRawBody()
PreRequestFilters.Insert(0, (httpReq, httpRes) => { httpReq.UseBufferedStream = true; });
}
Note: Adding this filter has implications for all the operations in your service. See this forum post on the side-effects of this filter, and suggestion to minimize them.
Then check the signature in your service operations manually like this:
internal class MyService : Service
{
public void Post(HelloDto request)
{
var signature = Request.Headers[WebhookEventConstants.SecretSignatureHeaderName]; // "X-Hub-Signature"
if (signature == null)
{
// The signature was not present, throw or return!
throw new HttpError(HttpStatusCode.Unauthorized);
}
if (!Request.VerifySignature(signature, "YOURBASE64ENCODEDSECRET"))
{
// This signature was invalid, throw or return!
throw new HttpError(HttpStatusCode.Unauthorized);
}
// They said hello!
var message = request.Text;
}
}
WARNING: We recommend that you store the secret somewhere safe, because if an attacker can discover your secret, they can spoof the service that raises events to your endpoint and this security measure is compromised.
First:
Install-Package ServiceStack.Webhooks.Subscribers
Then add the new HmacAuthProvider
to your AuthFeature
, and add the special PreRequestFilter.
public override void Configure(Container container)
{
Plugins.Add(new AuthFeature(() => new AuthUserSession(), new IAuthProvider[]
{
// Any other providers you might need
new HmacAuthProvider
{
Secret = "yourbase64encodedsecret",
// OR for dynamic values looked up from a service, or based upon the name of the event:
//OnGetSecret = (request, name) => LookupSecret(name)
}
}));
// We need this filter to allow us to read the request body in IRequest.GetRawBody()
PreRequestFilters.Insert(0, (httpReq, httpRes) => { httpReq.UseBufferedStream = true; });
}
Note: Adding this filter has implications for all the operations in your service. See this forum post on the side-effects of this filter, and suggestion to minimize them.
IMPORTANT: Make sure you decorate your request DTO or Service Operation with the [Authenticate]
attribute too!
WARNING: We recommend that you store the secret somewhere safe, because if an attacker can discover your secret, they can spoof the service that raises events to your endpoint and this security measure is compromised.