Github Webhook Security
Securing GitHub webhooks is essential to ensure only valid requests from GitHub reach your server. Here’s a guide to implementing security best practices when using GitHub webhooks:
1. Use a Secret Key for Signature Verification
GitHub allows you to configure a secret key when setting up a webhook. This secret is used to generate a unique signature for each webhook request, which you can verify to confirm the request's authenticity.
Steps to Set Up a Secret Key:
- Set a Secret Key in GitHub: When creating the webhook in GitHub, add a unique secret key (e.g.,
my_super_secret_key
) in the "Secret" field. - Verify the Signature on Your Server:
- Every webhook payload from GitHub includes a
X-Hub-Signature-256
header containing a hash of the payload with the secret key. - Compute the hash on your server and compare it to the signature provided by GitHub.
- Every webhook payload from GitHub includes a
Example Code for Signature Verification in Node.js
This sample uses HMAC with SHA-256 for signature verification:
const crypto = require("crypto");
const express = require("express");
const app = express();
app.use(express.json()); // Parse JSON bodies
const GITHUB_SECRET = process.env.GITHUB_SECRET || "your_secret_key";
// Middleware to verify GitHub signature
function verifyGitHubSignature(req, res, next) {
const signature = req.headers["x-hub-signature-256"];
const computedHash = `sha256=${crypto
.createHmac("sha256", GITHUB_SECRET)
.update(JSON.stringify(req.body))
.digest("hex")}`;
if (signature !== computedHash) {
return res.status(401).send("Invalid signature");
}
next();
}
app.post("/github-webhook", verifyGitHubSignature, (req, res) => {
console.log("Verified request from GitHub.");
res.sendStatus(200);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
- How It Works: The code above reads the
X-Hub-Signature-256
header from the request and compares it to a computed hash of the request body using the shared secret key. If they don’t match, the request is rejected.
2. Restrict IP Ranges for GitHub Webhook Requests
GitHub’s webhooks are sent from specific IP ranges. Configuring your server or firewall to only allow requests from these ranges adds an extra layer of security.
- GitHub’s IP Ranges: GitHub publishes its webhook IP ranges, which you can retrieve from GitHub’s Meta API.
- Firewall Rules: Configure your server firewall to only accept requests from these IP addresses.
Example for restricting IPs on a server using iptables
:
# Allow GitHub’s IPs (replace with the actual IP ranges from GitHub Meta API)
sudo iptables -A INPUT -p tcp -s <GITHUB_IP_RANGE> --dport 3000 -j ACCEPT
# Block others
sudo iptables -A INPUT -p tcp --dport 3000 -j DROP
3. Use HTTPS for Webhook Endpoints
To protect the data being sent in webhook requests, always use HTTPS for your webhook endpoints. HTTPS ensures that data is encrypted in transit, preventing attackers from intercepting sensitive information like repository data and event details.
- Enable HTTPS: Obtain an SSL certificate for your server and configure it to only accept HTTPS connections.
- Use a Certificate Authority (CA): Get your certificate from a trusted CA, or use tools like Let's Encrypt to set up HTTPS for free.
4. Validate Event Types and Payload Content
Only accept events you are interested in and verify the expected structure of the data before processing it.
- Event Type Filtering: Check the
X-GitHub-Event
header to confirm the event type. Reject any unrecognized event types. - Payload Validation: Validate required fields in the payload to prevent errors or malicious injections.
Example in Express.js:
app.post("/github-webhook", verifyGitHubSignature, (req, res) => {
const eventType = req.headers["x-github-event"];
if (eventType === "push") {
// Process push event
console.log("Push event received:", req.body);
} else if (eventType === "pull_request") {
// Process pull request event
console.log("Pull request event received:", req.body);
} else {
return res.status(400).send("Unsupported event type");
}
res.sendStatus(200);
});
5. Rate Limit Webhook Endpoints
Implement rate limiting on your webhook endpoints to mitigate potential abuse. Rate limiting prevents malicious actors from overwhelming your server with repeated requests.
Example Using Express-Rate-Limit Middleware
const rateLimit = require("express-rate-limit");
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // limit each IP to 100 requests per windowMs
});
app.use("/github-webhook", limiter, verifyGitHubSignature, (req, res) => {
console.log("Received and verified GitHub webhook event.");
res.sendStatus(200);
});
6. Log Webhook Activity for Monitoring and Auditing
Log incoming webhook events, including timestamps, IP addresses, event types, and any verification failures. Logs help you identify and respond to suspicious activities.
- Log Important Headers: Capture the
X-GitHub-Event
,X-GitHub-Delivery
, and signature headers for each request. - Store Logs Securely: Ensure logs are stored securely and backed up for audit purposes.
Example of Logging
app.post("/github-webhook", verifyGitHubSignature, (req, res) => {
const eventType = req.headers["x-github-event"];
const deliveryID = req.headers["x-github-delivery"];
console.log(`[${new Date().toISOString()}] Event: ${eventType}, Delivery ID: ${deliveryID}`);
res.sendStatus(200);
});
7. Rotate Secret Keys Periodically
Periodically rotate your GitHub webhook secret key to minimize the impact of any potential key exposure. To rotate:
- Add a new secret in your GitHub webhook settings.
- Update your server code to handle requests signed with either the old or new secret during the transition.
- Once the transition is complete, remove the old secret from your code and GitHub.
By following these security practices, you’ll be able to protect your GitHub webhook endpoint from unauthorized access, reduce the risk of security breaches, and ensure the integrity of your webhook data.