Closed dennissivia closed 5 years ago
🙏 thanks for opening the issue Dennis!
I was not aware of the problem with escape sequences and JSON.parse
> JSON.parse('"\\u001B"')
< '\u001b'
ESC
?1: Will take a little bit of time, because I have to craft a signature, but I will create it asap
2: I triggered the issue by doing end-to-end requests, via the GitHub API via curl like this:
curl -i \
-H "Authorization: token $API_TOKEN" \
-H "Content-Type: application/vnd.github.v3+json" \
-d '{"body":"Foo\n\u001B[34mbar: \u2665\u2665\u2665\u2665\u2665\u2665\u2665\u2665\nthis-is-lost\u001b[0m\u001B[2K" }' \
https://api.github.com/repos/:owner/:repo/issues/:issueID/comments"
The bare minimum for a failing payload would be \u001B
aka ESC
. Github will make this a capital B before encoding, when the request is created via the API (like above) and thus it will fail, when webhook.js will use the JS representation (lowercase b) for the validation.
Thanks! I created a test comment at https://github.com/gr2m/sandbox/issues/78#issuecomment-481409917 and retrieved a payload via smee.io, but it looks like smee.io does the JSON transform before storing the webhooks
"body": "Foo\n\u001b[34mbar: ♥♥♥♥♥♥♥♥\nthis-is-lost\u001b[0m\u001b[2K"
{ "host": "smee.io", "connection": "close", "accept": "*/*", "user-agent": "GitHub-Hookshot/38ab7f6", "x-github-event": "issue_comment", "x-github-delivery": "1340a790-5b01-11e9-9e36-475fe7c6e71e", "content-type": "application/json", "x-hub-signature": "sha1=b56d20007f63e1042638b6ace91bb59e8ac4deed", "x-request-id": "e08b9687-b11f-48bf-82a9-8bb6efad73de", "x-forwarded-for": "192.30.252.45", "x-forwarded-proto": "https", "x-forwarded-port": "443", "via": "1.1 vegur", "connect-time": "1", "x-request-start": "1554839579071", "total-route-time": "0", "content-length": "8472", "body": { "action": "created", "issue": { "url": "https://api.github.com/repos/gr2m/sandbox/issues/78", "repository_url": "https://api.github.com/repos/gr2m/sandbox", "labels_url": "https://api.github.com/repos/gr2m/sandbox/issues/78/labels{/name}", "comments_url": "https://api.github.com/repos/gr2m/sandbox/issues/78/comments", "events_url": "https://api.github.com/repos/gr2m/sandbox/issues/78/events", "html_url": "https://github.com/gr2m/sandbox/issues/78", "id": 431161080, "node_id": "MDU6SXNzdWU0MzExNjEwODA=", "number": 78, "title": "Testing escape sequences in webhooks", "user": { "login": "gr2m", "id": 39992, "node_id": "MDQ6VXNlcjM5OTky", "avatar_url": "https://avatars3.githubusercontent.com/u/39992?v=4", "gravatar_id": "", "url": "https://api.github.com/users/gr2m", "html_url": "https://github.com/gr2m", "followers_url": "https://api.github.com/users/gr2m/followers", "following_url": "https://api.github.com/users/gr2m/following{/other_user}", "gists_url": "https://api.github.com/users/gr2m/gists{/gist_id}", "starred_url": "https://api.github.com/users/gr2m/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/gr2m/subscriptions", "organizations_url": "https://api.github.com/users/gr2m/orgs", "repos_url": "https://api.github.com/users/gr2m/repos", "events_url": "https://api.github.com/users/gr2m/events{/privacy}", "received_events_url": "https://api.github.com/users/gr2m/received_events", "type": "User", "site_admin": false }, "labels": [], "state": "open", "locked": false, "assignee": null, "assignees": [], "milestone": null, "comments": 0, "created_at": "2019-04-09T19:52:10Z", "updated_at": "2019-04-09T19:52:58Z", "closed_at": null, "author_association": "OWNER", "body": "https://github.com/octokit/webhooks.js/issues/71#issuecomment-481394835" }, "comment": { "url": "https://api.github.com/repos/gr2m/sandbox/issues/comments/481409917", "html_url": "https://github.com/gr2m/sandbox/issues/78#issuecomment-481409917", "issue_url": "https://api.github.com/repos/gr2m/sandbox/issues/78", "id": 481409917, "node_id": "MDEyOklzc3VlQ29tbWVudDQ4MTQwOTkxNw==", "user": { "login": "gr2m", "id": 39992, "node_id": "MDQ6VXNlcjM5OTky", "avatar_url": "https://avatars3.githubusercontent.com/u/39992?v=4", "gravatar_id": "", "url": "https://api.github.com/users/gr2m", "html_url": "https://github.com/gr2m", "followers_url": "https://api.github.com/users/gr2m/followers", "following_url": "https://api.github.com/users/gr2m/following{/other_user}", "gists_url": "https://api.github.com/users/gr2m/gists{/gist_id}", "starred_url": "https://api.github.com/users/gr2m/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/gr2m/subscriptions", "organizations_url": "https://api.github.com/users/gr2m/orgs", "repos_url": "https://api.github.com/users/gr2m/repos", "events_url": "https://api.github.com/users/gr2m/events{/privacy}", "received_events_url": "https://api.github.com/users/gr2m/received_events", "type": "User", "site_admin": false }, "created_at": "2019-04-09T19:52:58Z", "updated_at": "2019-04-09T19:52:58Z", "author_association": "OWNER", "body": "Foo\n\u001b[34mbar: ♥♥♥♥♥♥♥♥\nthis-is-lost\u001b[0m\u001b[2K" }, "repository": { "id": 102985470, "node_id": "MDEwOlJlcG9zaXRvcnkxMDI5ODU0NzA=", "name": "sandbox", "full_name": "gr2m/sandbox", "private": false, "owner": { "login": "gr2m", "id": 39992, "node_id": "MDQ6VXNlcjM5OTky", "avatar_url": "https://avatars3.githubusercontent.com/u/39992?v=4", "gravatar_id": "", "url": "https://api.github.com/users/gr2m", "html_url": "https://github.com/gr2m", "followers_url": "https://api.github.com/users/gr2m/followers", "following_url": "https://api.github.com/users/gr2m/following{/other_user}", "gists_url": "https://api.github.com/users/gr2m/gists{/gist_id}", "starred_url": "https://api.github.com/users/gr2m/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/gr2m/subscriptions", "organizations_url": "https://api.github.com/users/gr2m/orgs", "repos_url": "https://api.github.com/users/gr2m/repos", "events_url": "https://api.github.com/users/gr2m/events{/privacy}", "received_events_url": "https://api.github.com/users/gr2m/received_events", "type": "User", "site_admin": false }, "html_url": "https://github.com/gr2m/sandbox", "description": "just playing around don’t mind me", "fork": false, "url": "https://api.github.com/repos/gr2m/sandbox", "forks_url": "https://api.github.com/repos/gr2m/sandbox/forks", "keys_url": "https://api.github.com/repos/gr2m/sandbox/keys{/key_id}", "collaborators_url": "https://api.github.com/repos/gr2m/sandbox/collaborators{/collaborator}", "teams_url": "https://api.github.com/repos/gr2m/sandbox/teams", "hooks_url": "https://api.github.com/repos/gr2m/sandbox/hooks", "issue_events_url": "https://api.github.com/repos/gr2m/sandbox/issues/events{/number}", "events_url": "https://api.github.com/repos/gr2m/sandbox/events", "assignees_url": "https://api.github.com/repos/gr2m/sandbox/assignees{/user}", "branches_url": "https://api.github.com/repos/gr2m/sandbox/branches{/branch}", "tags_url": "https://api.github.com/repos/gr2m/sandbox/tags", "blobs_url": "https://api.github.com/repos/gr2m/sandbox/git/blobs{/sha}", "git_tags_url": "https://api.github.com/repos/gr2m/sandbox/git/tags{/sha}", "git_refs_url": "https://api.github.com/repos/gr2m/sandbox/git/refs{/sha}", "trees_url": "https://api.github.com/repos/gr2m/sandbox/git/trees{/sha}", "statuses_url": "https://api.github.com/repos/gr2m/sandbox/statuses/{sha}", "languages_url": "https://api.github.com/repos/gr2m/sandbox/languages", "stargazers_url": "https://api.github.com/repos/gr2m/sandbox/stargazers", "contributors_url": "https://api.github.com/repos/gr2m/sandbox/contributors", "subscribers_url": "https://api.github.com/repos/gr2m/sandbox/subscribers", "subscription_url": "https://api.github.com/repos/gr2m/sandbox/subscription", "commits_url": "https://api.github.com/repos/gr2m/sandbox/commits{/sha}", "git_commits_url": "https://api.github.com/repos/gr2m/sandbox/git/commits{/sha}", "comments_url": "https://api.github.com/repos/gr2m/sandbox/comments{/number}", "issue_comment_url": "https://api.github.com/repos/gr2m/sandbox/issues/comments{/number}", "contents_url": "https://api.github.com/repos/gr2m/sandbox/contents/{+path}", "compare_url": "https://api.github.com/repos/gr2m/sandbox/compare/{base}...{head}", "merges_url": "https://api.github.com/repos/gr2m/sandbox/merges", "archive_url": "https://api.github.com/repos/gr2m/sandbox/{archive_format}{/ref}", "downloads_url": "https://api.github.com/repos/gr2m/sandbox/downloads", "issues_url": "https://api.github.com/repos/gr2m/sandbox/issues{/number}", "pulls_url": "https://api.github.com/repos/gr2m/sandbox/pulls{/number}", "milestones_url": "https://api.github.com/repos/gr2m/sandbox/milestones{/number}", "notifications_url": "https://api.github.com/repos/gr2m/sandbox/notifications{?since,all,participating}", "labels_url": "https://api.github.com/repos/gr2m/sandbox/labels{/name}", "releases_url": "https://api.github.com/repos/gr2m/sandbox/releases{/id}", "deployments_url": "https://api.github.com/repos/gr2m/sandbox/deployments", "created_at": "2017-09-09T21:16:50Z", "updated_at": "2019-02-24T22:41:09Z", "pushed_at": "2019-03-04T02:11:56Z", "git_url": "git://github.com/gr2m/sandbox.git", "ssh_url": "git@github.com:gr2m/sandbox.git", "clone_url": "https://github.com/gr2m/sandbox.git", "svn_url": "https://github.com/gr2m/sandbox", "homepage": null, "size": 29, "stargazers_count": 1, "watchers_count": 1, "language": null, "has_issues": true, "has_projects": true, "has_downloads": true, "has_wiki": false, "has_pages": false, "forks_count": 1, "mirror_url": null, "archived": false, "disabled": false, "open_issues_count": 5, "license": null, "forks": 1, "open_issues": 5, "watchers": 1, "default_branch": "latest" }, "sender": { "login": "gr2m", "id": 39992, "node_id": "MDQ6VXNlcjM5OTky", "avatar_url": "https://avatars3.githubusercontent.com/u/39992?v=4", "gravatar_id": "", "url": "https://api.github.com/users/gr2m", "html_url": "https://github.com/gr2m", "followers_url": "https://api.github.com/users/gr2m/followers", "following_url": "https://api.github.com/users/gr2m/following{/other_user}", "gists_url": "https://api.github.com/users/gr2m/gists{/gist_id}", "starred_url": "https://api.github.com/users/gr2m/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/gr2m/subscriptions", "organizations_url": "https://api.github.com/users/gr2m/orgs", "repos_url": "https://api.github.com/users/gr2m/repos", "events_url": "https://api.github.com/users/gr2m/events{/privacy}", "received_events_url": "https://api.github.com/users/gr2m/received_events", "type": "User", "site_admin": false }, "installation": { "id": 743367, "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNzQzMzY3" } }, "query": {}, "timestamp": 1554839579075 }
I'll try a minimal setup to retrieve the webhook without using smee.io, just using bare Node.js
I was able to confirm it using a simple server hosted on Glitch: https://glitch.com/edit/#!/rocky-den?path=server.js:17:27
"body": "Foo\n\u001B[34mbar: ♥♥♥♥♥♥♥♥\nthis-is-lost\u001B[0m\u001B[2K"
{ "action": "created", "issue": { "url": "https://api.github.com/repos/gr2m/sandbox/issues/78", "repository_url": "https://api.github.com/repos/gr2m/sandbox", "labels_url": "https://api.github.com/repos/gr2m/sandbox/issues/78/labels{/name}", "comments_url": "https://api.github.com/repos/gr2m/sandbox/issues/78/comments", "events_url": "https://api.github.com/repos/gr2m/sandbox/issues/78/events", "html_url": "https://github.com/gr2m/sandbox/issues/78", "id": 431161080, "node_id": "MDU6SXNzdWU0MzExNjEwODA=", "number": 78, "title": "Testing escape sequences in webhooks", "user": { "login": "gr2m", "id": 39992, "node_id": "MDQ6VXNlcjM5OTky", "avatar_url": "https://avatars3.githubusercontent.com/u/39992?v=4", "gravatar_id": "", "url": "https://api.github.com/users/gr2m", "html_url": "https://github.com/gr2m", "followers_url": "https://api.github.com/users/gr2m/followers", "following_url": "https://api.github.com/users/gr2m/following{/other_user}", "gists_url": "https://api.github.com/users/gr2m/gists{/gist_id}", "starred_url": "https://api.github.com/users/gr2m/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/gr2m/subscriptions", "organizations_url": "https://api.github.com/users/gr2m/orgs", "repos_url": "https://api.github.com/users/gr2m/repos", "events_url": "https://api.github.com/users/gr2m/events{/privacy}", "received_events_url": "https://api.github.com/users/gr2m/received_events", "type": "User", "site_admin": false }, "labels": [], "state": "open", "locked": false, "assignee": null, "assignees": [], "milestone": null, "comments": 1, "created_at": "2019-04-09T19:52:10Z", "updated_at": "2019-04-09T20:14:44Z", "closed_at": null, "author_association": "OWNER", "body": "https://github.com/octokit/webhooks.js/issues/71#issuecomment-481394835" }, "comment": { "url": "https://api.github.com/repos/gr2m/sandbox/issues/comments/481417364", "html_url": "https://github.com/gr2m/sandbox/issues/78#issuecomment-481417364", "issue_url": "https://api.github.com/repos/gr2m/sandbox/issues/78", "id": 481417364, "node_id": "MDEyOklzc3VlQ29tbWVudDQ4MTQxNzM2NA==", "user": { "login": "gr2m", "id": 39992, "node_id": "MDQ6VXNlcjM5OTky", "avatar_url": "https://avatars3.githubusercontent.com/u/39992?v=4", "gravatar_id": "", "url": "https://api.github.com/users/gr2m", "html_url": "https://github.com/gr2m", "followers_url": "https://api.github.com/users/gr2m/followers", "following_url": "https://api.github.com/users/gr2m/following{/other_user}", "gists_url": "https://api.github.com/users/gr2m/gists{/gist_id}", "starred_url": "https://api.github.com/users/gr2m/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/gr2m/subscriptions", "organizations_url": "https://api.github.com/users/gr2m/orgs", "repos_url": "https://api.github.com/users/gr2m/repos", "events_url": "https://api.github.com/users/gr2m/events{/privacy}", "received_events_url": "https://api.github.com/users/gr2m/received_events", "type": "User", "site_admin": false }, "created_at": "2019-04-09T20:14:44Z", "updated_at": "2019-04-09T20:14:44Z", "author_association": "OWNER", "body": "Foo\n\u001B[34mbar: ♥♥♥♥♥♥♥♥\nthis-is-lost\u001B[0m\u001B[2K" }, "repository": { "id": 102985470, "node_id": "MDEwOlJlcG9zaXRvcnkxMDI5ODU0NzA=", "name": "sandbox", "full_name": "gr2m/sandbox", "private": false, "owner": { "login": "gr2m", "id": 39992, "node_id": "MDQ6VXNlcjM5OTky", "avatar_url": "https://avatars3.githubusercontent.com/u/39992?v=4", "gravatar_id": "", "url": "https://api.github.com/users/gr2m", "html_url": "https://github.com/gr2m", "followers_url": "https://api.github.com/users/gr2m/followers", "following_url": "https://api.github.com/users/gr2m/following{/other_user}", "gists_url": "https://api.github.com/users/gr2m/gists{/gist_id}", "starred_url": "https://api.github.com/users/gr2m/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/gr2m/subscriptions", "organizations_url": "https://api.github.com/users/gr2m/orgs", "repos_url": "https://api.github.com/users/gr2m/repos", "events_url": "https://api.github.com/users/gr2m/events{/privacy}", "received_events_url": "https://api.github.com/users/gr2m/received_events", "type": "User", "site_admin": false }, "html_url": "https://github.com/gr2m/sandbox", "description": "just playing around don’t mind me", "fork": false, "url": "https://api.github.com/repos/gr2m/sandbox", "forks_url": "https://api.github.com/repos/gr2m/sandbox/forks", "keys_url": "https://api.github.com/repos/gr2m/sandbox/keys{/key_id}", "collaborators_url": "https://api.github.com/repos/gr2m/sandbox/collaborators{/collaborator}", "teams_url": "https://api.github.com/repos/gr2m/sandbox/teams", "hooks_url": "https://api.github.com/repos/gr2m/sandbox/hooks", "issue_events_url": "https://api.github.com/repos/gr2m/sandbox/issues/events{/number}", "events_url": "https://api.github.com/repos/gr2m/sandbox/events", "assignees_url": "https://api.github.com/repos/gr2m/sandbox/assignees{/user}", "branches_url": "https://api.github.com/repos/gr2m/sandbox/branches{/branch}", "tags_url": "https://api.github.com/repos/gr2m/sandbox/tags", "blobs_url": "https://api.github.com/repos/gr2m/sandbox/git/blobs{/sha}", "git_tags_url": "https://api.github.com/repos/gr2m/sandbox/git/tags{/sha}", "git_refs_url": "https://api.github.com/repos/gr2m/sandbox/git/refs{/sha}", "trees_url": "https://api.github.com/repos/gr2m/sandbox/git/trees{/sha}", "statuses_url": "https://api.github.com/repos/gr2m/sandbox/statuses/{sha}", "languages_url": "https://api.github.com/repos/gr2m/sandbox/languages", "stargazers_url": "https://api.github.com/repos/gr2m/sandbox/stargazers", "contributors_url": "https://api.github.com/repos/gr2m/sandbox/contributors", "subscribers_url": "https://api.github.com/repos/gr2m/sandbox/subscribers", "subscription_url": "https://api.github.com/repos/gr2m/sandbox/subscription", "commits_url": "https://api.github.com/repos/gr2m/sandbox/commits{/sha}", "git_commits_url": "https://api.github.com/repos/gr2m/sandbox/git/commits{/sha}", "comments_url": "https://api.github.com/repos/gr2m/sandbox/comments{/number}", "issue_comment_url": "https://api.github.com/repos/gr2m/sandbox/issues/comments{/number}", "contents_url": "https://api.github.com/repos/gr2m/sandbox/contents/{+path}", "compare_url": "https://api.github.com/repos/gr2m/sandbox/compare/{base}...{head}", "merges_url": "https://api.github.com/repos/gr2m/sandbox/merges", "archive_url": "https://api.github.com/repos/gr2m/sandbox/{archive_format}{/ref}", "downloads_url": "https://api.github.com/repos/gr2m/sandbox/downloads", "issues_url": "https://api.github.com/repos/gr2m/sandbox/issues{/number}", "pulls_url": "https://api.github.com/repos/gr2m/sandbox/pulls{/number}", "milestones_url": "https://api.github.com/repos/gr2m/sandbox/milestones{/number}", "notifications_url": "https://api.github.com/repos/gr2m/sandbox/notifications{?since,all,participating}", "labels_url": "https://api.github.com/repos/gr2m/sandbox/labels{/name}", "releases_url": "https://api.github.com/repos/gr2m/sandbox/releases{/id}", "deployments_url": "https://api.github.com/repos/gr2m/sandbox/deployments", "created_at": "2017-09-09T21:16:50Z", "updated_at": "2019-02-24T22:41:09Z", "pushed_at": "2019-03-04T02:11:56Z", "git_url": "git://github.com/gr2m/sandbox.git", "ssh_url": "git@github.com:gr2m/sandbox.git", "clone_url": "https://github.com/gr2m/sandbox.git", "svn_url": "https://github.com/gr2m/sandbox", "homepage": null, "size": 29, "stargazers_count": 1, "watchers_count": 1, "language": null, "has_issues": true, "has_projects": true, "has_downloads": true, "has_wiki": false, "has_pages": false, "forks_count": 1, "mirror_url": null, "archived": false, "disabled": false, "open_issues_count": 5, "license": null, "forks": 1, "open_issues": 5, "watchers": 1, "default_branch": "latest" }, "sender": { "login": "gr2m", "id": 39992, "node_id": "MDQ6VXNlcjM5OTky", "avatar_url": "https://avatars3.githubusercontent.com/u/39992?v=4", "gravatar_id": "", "url": "https://api.github.com/users/gr2m", "html_url": "https://github.com/gr2m", "followers_url": "https://api.github.com/users/gr2m/followers", "following_url": "https://api.github.com/users/gr2m/following{/other_user}", "gists_url": "https://api.github.com/users/gr2m/gists{/gist_id}", "starred_url": "https://api.github.com/users/gr2m/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/gr2m/subscriptions", "organizations_url": "https://api.github.com/users/gr2m/orgs", "repos_url": "https://api.github.com/users/gr2m/repos", "events_url": "https://api.github.com/users/gr2m/events{/privacy}", "received_events_url": "https://api.github.com/users/gr2m/received_events", "type": "User", "site_admin": false }, "installation": { "id": 743367, "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNzQzMzY3" } }
@gr2m the main issue is that the library is assuming that JSON.stringify()
will generate the same output than the original raw payload. And that can break at any moment. If github starts to send the JSON prettified, the current implementation would break for example. If github starts to add spaces between array items, the signature implementation will break, and so on. It is possible that the current implementation is failing not only for ansi codes but for other characters, because you can escape any character in JSON: it's up to the implementation to decide which ones should be escaped. Try this:
console.log(JSON.parse('{"foo":"\\u0061"}'))
It's just the character a
, but escaped.
The solution is to always calculate the signature from the original, raw, payload. Not the result of stringifying the data. In fact, all the functions: sign
, verify
, verifyAndReceive
,... should deprecate their support for signing objects and only support strings or buffers.
Got it, that all makes sense 🤔 I’ll need to think about how to migrate to a new version
@gr2m I think the library should print a deprecation warning if it receives something that is neither a string nor a buffer. Also, the payload should always be read like this: https://github.com/octokit/webhooks.js/blob/c0fade79854b19efda2b837356741b0eceb94c54/middleware/get-payload.js#L13-L16 and keep the data
somewhere, and that might break apps using body-parser or any middleware that reads the body before webhooks.js
The quick and dirty fix is to do this here: https://github.com/octokit/webhooks.js/blob/17ec6e3dc434d39caf542efba017509339be8a52/sign/index.js#L10
JSON.stringify(payload).replace(/\\u[\da-z]{4}/g, s => s.toUpperCase())
But whenever possible it should always use the raw payload instead. Because this fix does not guarantee that the result is going to be the same than the original payload. However, at least, fixes the known problem with the ESC character used in ansi codes.
The quick and dirty fix is to do this here:
👍
Yeah, I was thinking about the same. For now this would solve the problem without breaking existing apps. However I would like to also add a deprecation message with the same change, as well as pointing out that the support for also having body-parser
or similar solutions at the same time is also deprecated.
After that we can consider a major release (after the release with the bugfix) that works the other way around or even is incompatible with other body-parsers. (like throwing an exception if the content is already read and parsed). This should be another issue/pr however so that we can ship this change in a pragmatic and fast fashion. Would you be fine with that @gr2m ?
Yes that sounds good!
We can move forward with the quickfix by @gimenete! Though I think it will uppercase the \u
prefix, too, not only the 4 characters after. Also I think the sequence is hexidecimal, so the right regex would be /\\u[\da-f]{4}/g
, right?
I’ve started a pull request at https://github.com/octokit/webhooks.js/pull/72, I want to verify it with a test app before merging.
Right now I wonder how that would work with Amazon Lambda / Firebase Functions etc? I think we get a request.rawBody
property, but I’m not yet sure how that would work, I haven’t worked with these environments myself yet.
I’d like to figure that out before moving forward with the deprecation and then the breaking change.
https://github.com/octokit/webhooks.js/pull/72 is ready for review, could you have a look if it looks good?
This change causes an unfortunate side effect. If the payload contains strings that start with \u
, the replacer kicks in and signature validation fails. I ran into this issue in a dependabot PR, which contained the following text:
Fix: exclude
\u000d
so new line won't convert to text
I quick-fixed this by changing the replacer to:
function toNormalizedJsonString (payload) {
return JSON.stringify(payload).replace(/[^\\]\\u[\da-f]{4}/g, s => {
return s.substr(0, 3) + s.substr(3).toUpperCase()
})
}
This is still not the perfect solution, but I guess it helps? I'll make a PR with this (including tests).
This will probably fail if the text ends in \
followed by an escape sequence. Unsure if this is addressable with a single regular expression.
@hugopeixoto could you please start a separate issue with a reproducible test case? You can use https://runkit.com/ to create it. Or even better, send a pull request with a failing test, and we take it from there?
Summary
verifyAndReceive
uses a parsed representation of the passed json data. This can lead to false negative signature verifications. One Example is the signature verification failure for ANSI/ISO-6429 codes a.k.a. escape sequences. If Any GitHub event body contains an escape sequence, the verification will always fail, even though everything is valid.Details
When the
ESC
sequence is json-encoded it might be encoded as\u001b
OR\u001B
. The node json parser will always encode this sequence with a lowercaseb
, which means that the verification will fail if the signature was generated with a capitalB
(which is already the case). Since this (and other encoding details) are ambiguous in the json standard, the only safe option is to verify the signature with the raw body we receive and not with the parsed representation, sinceJSON.stringify(JSON.parse(x))
does not necessarily yieldx
, thus causing a false negative. This however is only one example, that already happens. In general, the only way that will guarantee that we properly verify the signature header is, using the data in the format it was encoded in the first place. This means, before parsing it.Suggestion
In order to solve the problem, middleware/middleware might need to be adjusted, together with
get-payload
andverify-and-receive
. I was already working on that when a realised the following compatibility issues:Compatibility concerns
verifyAndReceived
is part of the public API, meaning people are using it directly and outside of the middleware context. (I saw it in some PR discussions). This means this function has to stay and still work with parsed data instead of json.request.body
It seems as if we need to keep the verification based on parsed data, if we want to support the mix of
body-parser
and webhook.js. We could also store the raw body in the request object.A compromise could be: testing if
req.body
is defined and if that is the case, we could use the current implementation. Also keep the current implementation ofverifyAndReceive
exposed and (maybe?) deprecate it. In order to fix our bug, we could just add a second verify and receive implementation that does the verification with the raw data. This however would duplicate a lot of code and I am not sure what you think about this.I hope we can figure out a way to fix this bug. This is just my view on things, maybe somebody has a better suggestion on how to address this issue.
Related issues
/cc @gr2m