Custom Webhooks
Webhooks are currently only available in certain Hosted CTFd tiers and CTFd Enterprise.
Overview
Custom Webhooks are HTTP requests sent by CTFd to any endpoints you choose when certain events occur within the CTFd application.
CTFd webhooks go through an initial verification over GET
requests. Once the webhook endpoint is verified, subsequent events are sent over POST
requests.
Check out this tutorial on how to integrate custom webhooks in your CTFd instance.
Endpoint ownership
Webhook endpoints are first validated by CTFd to prove that you have ownership over the endpoint.
Each CTFd instance that has the Webhooks plugin contains a randomly generated Shared Secret, which is used to prove the ownership of the endpoint.
Validation
To validate an endpoint, CTFd will send a GET
request to the target webhook URL with an additional query parameter, token
. The endpoint must then construct a JSON response containing the token and the shared secret hashed with HMAC-SHA256.
For example with an endpoint of http://example.com/
, CTFd will send:
GET http://example.com/?token=2b00042f6
and your application should respond with
{"response": hmac_sha256(SHARED_SECRET, "2b00042f6")}
Events
Once the webhook endpoint is validated, you can then choose 4 event types that would trigger a POST
request to the endpoint:
- User Events - a request is sent to the webhook endpoint when a new user is created
- Team Events - a request is sent to the webhook endpoint when a new team is created
- Solve Events - a request is sent to the webhook endpoint when a challenge is solved
- First Blood Events - a request is sent to the webhook endpoint when a challenge is solved for the first time and the solving account is not hidden or banned.
Event Examples
Event Header Example
Below is an example of the headers sent by CTFd when sending a webhook. Events types are specified by the CTFd-Webhook-Event
header.
Host: example.com
User-Agent: CTFd-Webhook
Content-Length: 123
Accept: */*
Accept-Encoding: gzip, deflate
Content-Type: application/json
Ctfd-Webhook-Event: team.created
Ctfd-Webhook-Signature: t=1616995588,v1=9e8cdd253434b74119ff44a119bb2d93e3b78a3a6a1a52d71e1bf70c36c55234
Sentry-Trace: 3e8641d6696a4190ba634e35de7bcdtg-bv9f2938465208aa-1
X-Forwarded-For: 1.2.3.4
X-Forwarded-Proto: https
Request body
The request body contains the event body, particularly, in JSON format. The body will differ based on the event but in general will be the schema specified for the object in the REST API documentation
User Created Event
{
"country": None,
"website": None,
"oauth_id": None,
"name": "user",
"id": 1,
"fields": [],
"team_id": None,
"affiliation": None,
"bracket": None
}
Team Created Event
{
"country": None,
"website": None,
"oauth_id": None,
"name": "Team Name",
"id": 1,
"fields": [],
"captain_id": None,
"members": [],
"affiliation": None,
"bracket": None
}
Solve Event
{
"challenge": None,
"team": None,
"date": "2022-09-27T06:21:53.210169+00:00",
"type": "correct",
"challenge_id": 1,
"id": 1,
"user": None
}
First Blood Event
{
"challenge": None,
"team": None,
"date": "2022-09-27T05:55:38.106012+00:00",
"type": "correct",
"challenge_id": 1,
"id": 1,
"user": None
}
Security
It's important to ensure that your endpoint verifies that the data is coming from CTFd. If not, malicious users could potentially replay requests to your endpoint or trick your endpoint into doing something it isn't supposed to.
CTFd provides the CTFd-Webhook-Signature
header on every webhook sent out.
Take a look at this header taken from the example header above:
Ctfd-Webhook-Signature: t=1616995588,v1=9e8cdd253434b74119ff44a119bb2d93e3b78a3a6a1a52d71e1bf70c36c55234
The header is split up into two sections t
and v1
.
t
represents the unix time/epoch time when the webhook was sent out.
v1
represents an HMAC-SHA256 of the value of t
, a period ( . ) , and the request body, with the shared secret as the HMAC key.
For example, if your endpoint received a body of {'data': None, 'value': None}
then the signature would be made up of:
hmac_sha256(SHARED_SECRET, "1616995588.{'data': None, 'value': None}")
For optimal security, your endpoint should validate the signature of every request before using any of the data contained within. In addition, your endpoint can also choose to ignore requests whose signature is too old based on the timestamp.
Should an attacker attempt to manipulate the timestamp, request body, or signature, validation should fail as long as your shared secret hasn't been leaked.
Debugging
We provide simple debugging applications that implement the above security logic to help you debug/inspect webhook events. You can check out this tutorial on how to deploy them.
Questions/Feedback
Questions, feedback, and feature requests can be directed to https://ctfd.io/contact/.