From ee6d634be119fd3708f23d2721e6cbe7f9397b1d Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Wed, 28 Sep 2022 22:42:25 +0100 Subject: [PATCH] feat(ghbot): validate signatures coming in --- github-bot/package.json | 4 ++- github-bot/src/main.ts | 24 ++++++++++++--- pnpm-lock.yaml | 67 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 6 deletions(-) diff --git a/github-bot/package.json b/github-bot/package.json index 6f770bee5..3d300f80f 100644 --- a/github-bot/package.json +++ b/github-bot/package.json @@ -10,6 +10,8 @@ }, "dependencies": { "@octokit/webhooks": "10.1.5", + "@types/body-parser": "^1.19.2", + "body-parser": "^1.20.0", "dotenv": "16.0.0", "express": "4.17.2", "node-fetch": "2.6.7", @@ -17,8 +19,8 @@ }, "devDependencies": { "@types/express": "4.17.13", - "@types/node-fetch": "2.5.12", "@types/node": "^16.0.0", + "@types/node-fetch": "2.5.12", "typescript": "4.5.5" }, "engines": { diff --git a/github-bot/src/main.ts b/github-bot/src/main.ts index 2638ab85a..90d9a5992 100644 --- a/github-bot/src/main.ts +++ b/github-bot/src/main.ts @@ -1,15 +1,15 @@ /* eslint-disable no-console */ import { ProcessEnv } from "./config"; +import bodyParser from "body-parser"; import express from "express"; import fetch from "node-fetch"; +import crypto from "crypto"; import { URLSearchParams } from "url"; import type { EmitterWebhookEvent } from "@octokit/webhooks"; import type { Express } from "express"; export const app: Express = express(); -app.use(express.json()); - // Let NGINX work its magic. app.set("trust proxy", "loopback"); @@ -58,10 +58,24 @@ function ConvertGitHubURL(url: string) { /** * Listens for GitHub webhook calls. * + * @note THIS ENDPOINT DOES NOT PARSE REQ BODY INTO AN OBJECT! + * it instead leaves it as a raw string, which is necessary because GitHub have + * an insane secret checking process. + * * @name POST /webhook */ -app.post("/webhook", async (req, res) => { - console.dir(req.body); +app.post("/webhook", bodyParser.text({ type: "*/*" }), async (req, res) => { + const hash = crypto + .createHmac("SHA256", ProcessEnv.webhookSecret) + .update(req.body as string) + .digest("hex"); + + if (hash !== req.header("X-Hub-Signature-256")) { + return res.status(400).json({ + success: false, + description: `Invalid Signature.`, + }); + } const event = req.header("X-GitHub-Event"); @@ -72,7 +86,7 @@ app.post("/webhook", async (req, res) => { }); } - const body = req.body as EmitterWebhookEvent<"pull_request">["payload"]; + const body = JSON.parse(req.body) as EmitterWebhookEvent<"pull_request">["payload"]; if (body.action !== "opened" && body.action !== "edited") { return res.status(400).json({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4f6115e0..485b08d05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -279,9 +279,11 @@ importers: github-bot: specifiers: '@octokit/webhooks': 10.1.5 + '@types/body-parser': ^1.19.2 '@types/express': 4.17.13 '@types/node': 16.11.7 '@types/node-fetch': 2.5.12 + body-parser: ^1.20.0 dotenv: 16.0.0 express: 4.17.2 node-fetch: 2.6.7 @@ -289,6 +291,8 @@ importers: typescript: 4.5.5 dependencies: '@octokit/webhooks': 10.1.5 + '@types/body-parser': 1.19.2 + body-parser: 1.20.0 dotenv: 16.0.0 express: 4.17.2 node-fetch: 2.6.7 @@ -4460,6 +4464,26 @@ packages: - supports-color dev: true + /body-parser/1.20.0: + resolution: {integrity: sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.4 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.10.3 + raw-body: 2.5.1 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + /bonjour/3.5.0: resolution: {integrity: sha512-RaVTblr+OnEli0r/ud8InrU7D+G0y6aJhlxaLa6Pwty4+xoxboF1BsUI45tujvRpbj9dQVoglChqonGAsjEBYg==} dependencies: @@ -4712,6 +4736,11 @@ packages: resolution: {integrity: sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==} engines: {node: '>= 0.8'} + /bytes/3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + /cacache/12.0.4: resolution: {integrity: sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==} dependencies: @@ -5992,6 +6021,11 @@ packages: /destroy/1.0.4: resolution: {integrity: sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==} + /destroy/1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + /detect-libc/2.0.1: resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==} engines: {node: '>=8'} @@ -8030,6 +8064,17 @@ packages: statuses: 1.5.0 toidentifier: 1.0.1 + /http-errors/2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + /http-parser-js/0.5.8: resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} dev: true @@ -11664,6 +11709,13 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} dev: false + /qs/6.10.3: + resolution: {integrity: sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: false + /qs/6.11.0: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} @@ -11761,6 +11813,16 @@ packages: iconv-lite: 0.4.24 unpipe: 1.0.0 + /raw-body/2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + /rc/1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -13084,6 +13146,11 @@ packages: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} + /statuses/2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + /stream-browserify/2.0.2: resolution: {integrity: sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==} dependencies: