Skip to content

Commit b21ba52

Browse files
authored
Merge pull request #51 from rubberduck-vba/webhook
Avoid consuming body stream in auth handler
2 parents 1227984 + 752852b commit b21ba52

File tree

6 files changed

+60
-52
lines changed

6 files changed

+60
-52
lines changed

rubberduckvba.Server/Api/Admin/WebhookController.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.AspNetCore.Authorization;
22
using Microsoft.AspNetCore.Mvc;
3+
using System.Text.Json;
34

45
namespace rubberduckvba.Server.Api.Admin;
56

@@ -21,9 +22,11 @@ public WebhookController(
2122

2223
[Authorize("webhook")]
2324
[HttpPost("webhook/github")]
24-
public IActionResult GitHub([FromBody] PushWebhookPayload payload)
25+
public async Task<IActionResult> GitHub([FromBody] object body)
2526
{
26-
var eventType = _validator.Validate(payload, Request.Headers, out var content, out var gitref);
27+
var json = body.ToString();
28+
var payload = JsonSerializer.Deserialize<PushWebhookPayload>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
29+
var eventType = _validator.Validate(payload, json, Request.Headers, out var content, out var gitref);
2730

2831
if (eventType == WebhookPayloadType.Push)
2932
{

rubberduckvba.Server/Api/Admin/WebhookPayloadType.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
public enum WebhookPayloadType
44
{
5-
Unsupported,
5+
BadRequest,
66
Greeting,
77
Ping,
88
Push

rubberduckvba.Server/Api/Admin/WebhookPayloadValidationService.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
namespace rubberduckvba.Server.Api.Admin;
22

3-
public class WebhookPayloadValidationService(ConfigurationOptions options)
3+
public class WebhookPayloadValidationService(ConfigurationOptions options, WebhookSignatureValidationService signatureValidation)
44
{
5-
public WebhookPayloadType Validate(PushWebhookPayload payload, IHeaderDictionary headers, out string? content, out GitRef? gitref)
5+
public WebhookPayloadType Validate(PushWebhookPayload payload, string body, IHeaderDictionary headers, out string? content, out GitRef? gitref)
66
{
77
content = default;
8+
if (!signatureValidation.Validate(body, headers["X-Hub-Signature-256"].OfType<string>().ToArray()))
9+
{
10+
gitref = default;
11+
return WebhookPayloadType.BadRequest;
12+
}
813

914
gitref = new GitRef(payload.Ref);
1015
if (headers["X-GitHub-Event"].FirstOrDefault() == "ping")
@@ -21,6 +26,6 @@ public WebhookPayloadType Validate(PushWebhookPayload payload, IHeaderDictionary
2126
return WebhookPayloadType.Push;
2227
}
2328

24-
return WebhookPayloadType.Unsupported;
29+
return WebhookPayloadType.BadRequest;
2530
}
2631
}

rubberduckvba.Server/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,9 @@ private static void ConfigureServices(IServiceCollection services)
160160
services.AddSingleton<ConfigurationOptions>();
161161
services.AddSingleton<IMarkdownFormattingService, MarkdownFormattingService>();
162162
services.AddSingleton<ISyntaxHighlighterService, SyntaxHighlighterService>();
163-
services.AddSingleton<WebhookSignatureValidationService>();
163+
services.AddSingleton<WebhookHeaderValidationService>();
164164
services.AddSingleton<WebhookPayloadValidationService>();
165+
services.AddSingleton<WebhookSignatureValidationService>();
165166
services.AddSingleton<HangfireLauncherService>();
166167

167168
services.AddSingleton<IRubberduckDbService, RubberduckDbService>();

rubberduckvba.Server/WebhookAuthenticationHandler.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ namespace rubberduckvba.Server;
88

99
public class WebhookAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
1010
{
11-
private readonly WebhookSignatureValidationService _service;
11+
private readonly WebhookHeaderValidationService _service;
1212

1313
public WebhookAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder,
14-
WebhookSignatureValidationService service)
14+
WebhookHeaderValidationService service)
1515
: base(options, logger, encoder)
1616
{
1717
_service = service;
@@ -27,10 +27,7 @@ protected async override Task<AuthenticateResult> HandleAuthenticateAsync()
2727
var xHubSignature = Context.Request.Headers["X-Hub-Signature"].OfType<string>().ToArray();
2828
var xHubSignature256 = Context.Request.Headers["X-Hub-Signature-256"].OfType<string>().ToArray();
2929

30-
using var reader = new StreamReader(Context.Request.Body);
31-
var payload = reader.ReadToEndAsync().GetAwaiter().GetResult();
32-
33-
if (_service.Validate(payload, userAgent, xGitHubEvent, xGitHubDelivery, xHubSignature, xHubSignature256))
30+
if (_service.Validate(userAgent, xGitHubEvent, xGitHubDelivery, xHubSignature, xHubSignature256))
3431
{
3532
var principal = CreatePrincipal();
3633
var ticket = new AuthenticationTicket(principal, "webhook-signature");

rubberduckvba.Server/WebhookSignatureValidationService.cs renamed to rubberduckvba.Server/WebhookHeaderValidationService.cs

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,45 +7,8 @@ namespace rubberduckvba.Server;
77

88
public class WebhookSignatureValidationService(ConfigurationOptions configuration, ILogger<WebhookSignatureValidationService> logger)
99
{
10-
public bool Validate(
11-
string payload,
12-
string? userAgent,
13-
string[] xGitHubEvent,
14-
string[] xGitHubDelivery,
15-
string[] xHubSignature,
16-
string[] xHubSignature256
17-
)
10+
public bool Validate(string payload, string[] xHubSignature256)
1811
{
19-
if (!(userAgent ?? string.Empty).StartsWith("GitHub-Hookshot/"))
20-
{
21-
// user agent must be GitHub hookshot
22-
return false;
23-
}
24-
25-
if (!xGitHubEvent.Contains("push"))
26-
{
27-
if (xGitHubEvent.Contains("ping"))
28-
{
29-
// no harm just returning 200-OK on ping
30-
return true;
31-
}
32-
33-
// only authenticate ping and push events
34-
return false;
35-
}
36-
37-
if (!Guid.TryParse(xGitHubDelivery.SingleOrDefault(), out _))
38-
{
39-
// delivery should parse as a GUID
40-
return false;
41-
}
42-
43-
if (!xHubSignature.Any())
44-
{
45-
// SHA-1 signature header must be present
46-
return false;
47-
}
48-
4912
var signature = xHubSignature256.SingleOrDefault();
5013
if (signature == default)
5114
{
@@ -56,7 +19,7 @@ string[] xHubSignature256
5619
if (!IsValidSignature(signature, payload))
5720
{
5821
// SHA-256 signature must match
59-
//return false;
22+
return false;
6023
}
6124

6225
return true;
@@ -87,3 +50,42 @@ private bool IsValidSignature(string? signature, string payload)
8750
return (signature == check);
8851
}
8952
}
53+
54+
55+
public class WebhookHeaderValidationService(ConfigurationOptions configuration, ILogger<WebhookHeaderValidationService> logger)
56+
{
57+
public bool Validate(
58+
string? userAgent,
59+
string[] xGitHubEvent,
60+
string[] xGitHubDelivery,
61+
string[] xHubSignature,
62+
string[] xHubSignature256
63+
)
64+
{
65+
if (!(userAgent ?? string.Empty).StartsWith("GitHub-Hookshot/"))
66+
{
67+
// user agent must be GitHub hookshot
68+
return false;
69+
}
70+
71+
if (!xGitHubEvent.Contains("push"))
72+
{
73+
if (xGitHubEvent.Contains("ping"))
74+
{
75+
// no harm just returning 200-OK on ping
76+
return true;
77+
}
78+
79+
// only authenticate ping and push events
80+
return false;
81+
}
82+
83+
if (!Guid.TryParse(xGitHubDelivery.SingleOrDefault(), out _))
84+
{
85+
// delivery should parse as a GUID
86+
return false;
87+
}
88+
89+
return true;
90+
}
91+
}

0 commit comments

Comments
 (0)