Webhooks
Webhooks enable you to build your own real-time integrations that subscribe to certain report and program events on HackerOne. They can be used to:
- Update an external issue tracker
- Trigger a notification system
- Update a report's data backup
- Trigger provisioning for a user account
When one of those events is triggered, we'll send an HTTP POST payload to the webhook's configured URL.
Webhooks can be created on a program level and are bound to the permissions of the user that creates the webhook. Once configured, the webhook will be triggered each time one or more subscribed events occur.
Events
When configuring a webhook, you can choose which events you'd like to receive payloads for. Each event corresponds to a certain set of actions that can happen to your program and reports. It's best to subscribe to the specific events you plan to handle as it'll limit the number of HTTP requests to your server. For example, if you subscribe to the report_created
event, you'll then receive an HTTP request with a detailed payload every time a hacker submits a new vulnerability report to your program.
These are the available events:
Event | Description |
---|---|
report_agreed_on_going_public | Triggers when a report is agreed to be publicly disclosed. |
report_bounty_awarded | Triggers when a hacker is awarded a bounty as a result of their report. |
report_bounty_suggested | Triggers when a bounty is suggested against a report. |
report_closed_as_duplicate | Triggers when a report is closed as a duplicate of another report. |
report_closed_as_informative | Triggers when a report is closed as informative. |
report_closed_as_spam | Triggers when a report is closed as spam. |
report_closed_as_not_applicable | Triggers when a report is closed as not applicable. |
report_comment_created | Triggers each time a comment is created on a report. |
report_comments_closed | Triggers when a report is locked. |
report_created | Triggers each time a new vulnerability report is created. |
report_custom_field_value_updated | Triggers when a report custom field value is updated. |
report_needs_more_info | Triggers when a report is requiring new information. |
report_new | Triggers when a report has changed which requires program input. |
report_reopened | Triggers when a report is reopened. |
report_resolved | Triggers when a report is resolved. |
report_retest_approved | Triggers when report retest results are approved. |
report_retest_rejected | Triggers when report retest results are rejected. |
report_retest_user_completed | Triggers when report retest is completed by a hacker. |
report_retest_user_expired | Triggers when report retest user was removed from retest due to a timeout. |
report_retest_user_left | Triggers when report retest user left retest before completing. |
report_retesting | Triggers when report changed to retesting. |
report_triaged | Triggers when a report is triaged. |
report_group_assigned | Triggers when a report is assigned to another group. |
report_manually_disclosed | Triggers when a report is manually disclosed. |
report_not_eligible_for_bounty | Triggers when a report is marked as not eligible for bounty. |
report_became_public | Triggers when a report is made publicly accessible. |
report_undisclosed | Triggers when a report is undisclosed. |
report_swag_awarded | Triggers when a hacker is rewarded swag as a result of their report. |
report_user_assigned | Triggers when a user is assigned to a report. |
program_hacker_joined | Applies only to private programs. Triggers each time a hacker joins your program. |
program_hacker_left | Applies only to private programs. Triggers each time a hacker leaves your program. |
Payloads
Each webhook event has a predefined payload format with the relevant event information. Each payload contains the relevant report information and information about the reporter.
Delivery headers
Every HTTP request made to your webhooks' configured URL contains these special headers:
Header |
Description |
---|---|
X-H1-Event | The name of the event that triggered the delivery. |
X-H1-Delivery | A GUID to uniquely identify the delivery. |
X-H1-Signature | The HMAC hexdigest of the request body. If a secret is specified, the HMAC hexdigest is generated based on the request body using the SHA256 hash function and the secret as the HMAC key. If no secret is specified, the HMAC hexdigest is solely generated based on the request body using the SHA256 hash function. See the validating payloads section for more info. |
Example delivery
{
"data": {
"activity": {
"type": "activity-bug-filed",
"id": "1337",
"attributes": {
"message": "",
"created_at": "2020-02-25T08:05:21.674Z",
"updated_at": "2020-02-25T08:05:21.674Z",
"internal": false
},
"relationships": {
"actor": {
"data": {
"id": "1",
"type": "user",
"attributes": {
"username": "hacker",
"name": "Hacker One",
"disabled": false,
"created_at": "2019-08-01T13:53:04.239Z",
"profile_picture": {
"62x62": "/assets/avatars/default.png",
"82x82": "/assets/avatars/default.png",
"110x110": "/assets/avatars/default.png",
"260x260": "/assets/avatars/default.png"
},
"signal": null,
"impact": null,
"reputation": 107,
"bio": null,
"website": null,
"location": null,
"hackerone_triager": null
}
}
}
}
},
"report": {
"id": "548",
"type": "report",
"attributes": {
"title": "Critical vulnerability!",
"state": "new",
"created_at": "2020-02-25T08:05:21.405Z",
"vulnerability_information": "## Summary:\n[add summary of the vulnerability]\n\n## Steps To Reproduce:\n[add details for how we can reproduce the issue]\n\n 1. [add step]\n 1. [add step]\n 1. [add step]\n\n## Supporting Material/References:\n[list any additional material (e.g. screenshots, logs, etc.)]\n\n * [attachment / reference]\n\n## Impact\n\nThe trouble I've seen",
"triaged_at": null,
"closed_at": null,
"last_reporter_activity_at": "2020-02-25T08:05:21.674Z",
"first_program_activity_at": "2020-02-25T08:05:21.674Z",
"last_program_activity_at": "2020-02-25T08:05:21.674Z",
"bounty_awarded_at": null,
"swag_awarded_at": null,
"disclosed_at": null,
"reporter_agreed_on_going_public_at": null,
"last_public_activity_at": "2020-02-25T08:05:21.674Z",
"last_activity_at": "2020-02-25T08:11:20.699Z",
"source": null
},
"relationships": {
"reporter": {
"data": {
"id": "1",
"type": "user",
"attributes": {
"username": "hacker",
"name": "HackerOne",
"disabled": false,
"created_at": "2019-08-01T13:53:04.239Z",
"profile_picture": {
"62x62": "/assets/avatars/default.png",
"82x82": "/assets/avatars/default.png",
"110x110": "/assets/avatars/default.png",
"260x260": "/assets/avatars/default.png"
},
"signal": null,
"impact": null,
"reputation": null,
"bio": null,
"website": "http://hackerone.com",
"location": null,
"hackerone_triager": null
}
}
},
"severity": {
"data": {
"id": "1",
"type": "severity",
"attributes": {
"rating": "low",
"author_type": "User",
"user_id": "1",
"created_at": "2020-02-25T08:05:21.405Z"
}
}
}
}
}
}
}
Validating payloads from HackerOne
To validate that a request originated from HackerOne, we suggest that you provide a secret for all webhooks set up on our platform.
This secret is used to generate the X-H1-Signature
header containing the HMAC hexdigest of the request body signed with the
provided secret as the key. If the secret is not provided, the signature is generated with an empty string key (''
).
To verify the validity of any given request, the goal is to generate a hash of the body on your end-service using the shared secret, and validate this against the given X-H1-SIGNATURE
header.
If a simple endpoint is listening to our incoming webhooks, our requests can be verified by hash validation against our signature as shown in these implementation examples:
Python:
Assuming that the request
is a request with an accessible JSON format body
attribute:
import json
import hmac
def validate_request(request, secret, signature):
[ _, digest ] = signature.split('=')
generated_digest = hmac.new(secret.encode(), request['body'].encode(), "sha256").hexdigest()
return hmac.compare_digest(digest, generated_digest)
Ruby:
A similar implementation in Ruby (making use of Racks secure_compare
constant time comparator):
def validate_request(body, secret, signature)
_, digest = signature.split("=")
generated_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, body)
return halt 500, "Invalid signature!" unless Rack::Utils.secure_compare(generated_digest, digest)
end
Javascript:
A similar implementation in Javascript using Node.js HTTP server
var http = require('http');
var crypto = require('crypto');
const secret = "mysecret"
http.createServer(function (req, res) {
let sig = req.headers['x-h1-signature'].split('=')[1]
let data = []
let valid_signature = false
req.on('data', chunk => {
data.push(chunk)
})
req.on('end', () => {
const hash = crypto.createHmac('sha256', secret).update(data[0]).digest('hex')
valid_signature = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(hash));
res.writeHead(200, {'Content-Type': 'text/plain'});
let json_hash = {
"signature_valid": valid_signature
}
res.end(JSON.stringify(json_hash))
})
}).listen(7777)
Although implementations may differ, there are two key points to keep in mind regarding implementations:
- In the examples above, the first element of the signature split,
_
, is unused but will for any request from HackerOne always besha256
, regardless of which implementation is used. This is signified by characters before the=
in the signature (eg:sha256=bc3a344f9214bff0100b4dfc9eec47aa281935bfc0da0db1575ae7d4204eefe2
). - Although a
==
comparison may be sufficient to achieve the goal of validating the signature, it is recommended instead to use built-in constant time comparison methods such asRack::Utils.secure_compare
orhmac.compare_digest
, as not doing so could leave the secret vulnerable to a timing attack.