From 651073f69f96e994fc08025e613fdd65aac365b8 Mon Sep 17 00:00:00 2001 From: rxdn <29165304+rxdn@users.noreply.github.com> Date: Thu, 8 Jun 2023 20:45:05 +0100 Subject: [PATCH] Add host check --- .../api/integrations/activateintegration.go | 22 +++++++++++++++- .../api/integrations/createintegration.go | 26 +++++++++++++++++++ .../api/integrations/updateintegration.go | 13 ++++++++++ .../src/views/integrations/Activate.svelte | 6 ++++- 4 files changed, 65 insertions(+), 2 deletions(-) diff --git a/app/http/endpoints/api/integrations/activateintegration.go b/app/http/endpoints/api/integrations/activateintegration.go index f49eff9..99d7a78 100644 --- a/app/http/endpoints/api/integrations/activateintegration.go +++ b/app/http/endpoints/api/integrations/activateintegration.go @@ -2,11 +2,13 @@ package api import ( "encoding/json" + "fmt" dbclient "github.com/TicketsBot/GoPanel/database" "github.com/TicketsBot/GoPanel/utils" "github.com/gin-gonic/gin" "net/http" "strconv" + "strings" ) type activateIntegrationBody struct { @@ -77,6 +79,7 @@ func ActivateIntegrationHandler(ctx *gin.Context) { // Since we've checked the length, we can just iterate over the secrets and they're guaranteed to be correct secretMap := make(map[int]string) + secretValues := make(map[string]string) for secretName, value := range data.Secrets { if len(value) == 0 || len(value) > 255 { ctx.JSON(400, utils.ErrorStr("Secret values must be between 1 and 255 characters")) @@ -90,6 +93,7 @@ func ActivateIntegrationHandler(ctx *gin.Context) { if secret.Name == secretName { found = true secretMap[secret.Id] = value + secretValues[secret.Name] = value break inner } } @@ -102,7 +106,23 @@ func ActivateIntegrationHandler(ctx *gin.Context) { // Validate secrets if integration.Public && integration.Approved && integration.ValidationUrl != nil { - res, statusCode, err := utils.SecureProxyClient.DoRequest(http.MethodPost, *integration.ValidationUrl, nil, data.Secrets) + integrationHeaders, err := dbclient.Client.CustomIntegrationHeaders.GetByIntegration(integrationId) + if err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + headers := make(map[string]string) + for _, header := range integrationHeaders { + value := header.Value + for key, secret := range secretValues { + value = strings.ReplaceAll(value, fmt.Sprintf("%%%s%%", key), secret) + } + + headers[header.Name] = value + } + + res, statusCode, err := utils.SecureProxyClient.DoRequest(http.MethodPost, *integration.ValidationUrl, headers, secretValues) if err != nil { if statusCode == http.StatusRequestTimeout { ctx.JSON(400, utils.ErrorStr("Secret validation server did not respond in time (contact the integration author)")) diff --git a/app/http/endpoints/api/integrations/createintegration.go b/app/http/endpoints/api/integrations/createintegration.go index 4bfab02..48e203c 100644 --- a/app/http/endpoints/api/integrations/createintegration.go +++ b/app/http/endpoints/api/integrations/createintegration.go @@ -1,11 +1,13 @@ package api import ( + "errors" dbclient "github.com/TicketsBot/GoPanel/database" "github.com/TicketsBot/GoPanel/utils" "github.com/TicketsBot/database" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" + "strings" ) type integrationCreateBody struct { @@ -68,6 +70,19 @@ func CreateIntegrationHandler(ctx *gin.Context) { return } + if data.ValidationUrl != nil { + sameHost, err := isSameValidationUrlHost(data.WebhookUrl, *data.ValidationUrl) + if err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + if !sameHost { + ctx.JSON(400, utils.ErrorStr("Validation URL must be on the same host as the webhook URL")) + return + } + } + integration, err := dbclient.Client.CustomIntegrations.Create(userId, data.WebhookUrl, data.ValidationUrl, data.Method, data.Name, data.Description, data.ImageUrl, data.PrivacyPolicyUrl) if err != nil { ctx.JSON(500, utils.ErrorJson(err)) @@ -124,3 +139,14 @@ func CreateIntegrationHandler(ctx *gin.Context) { ctx.JSON(200, integration) } + +func isSameValidationUrlHost(webhookUrl, validationUrl string) (bool, error) { + webhookStripped := utils.GetUrlHost(strings.ReplaceAll(webhookUrl, "%", "")) + validationStripped := utils.GetUrlHost(strings.ReplaceAll(validationUrl, "%", "")) + + if webhookStripped == "Invalid URL" || validationStripped == "Invalid URL" { + return false, errors.New("invalid webhook or validation URL") + } + + return strings.ToLower(utils.SecondLevelDomain(webhookStripped)) == strings.ToLower(utils.SecondLevelDomain(validationStripped)), nil +} \ No newline at end of file diff --git a/app/http/endpoints/api/integrations/updateintegration.go b/app/http/endpoints/api/integrations/updateintegration.go index 002855c..5753129 100644 --- a/app/http/endpoints/api/integrations/updateintegration.go +++ b/app/http/endpoints/api/integrations/updateintegration.go @@ -88,6 +88,19 @@ func UpdateIntegrationHandler(ctx *gin.Context) { return } + if data.ValidationUrl != nil { + sameHost, err := isSameValidationUrlHost(data.WebhookUrl, *data.ValidationUrl) + if err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + if !sameHost { + ctx.JSON(400, utils.ErrorStr("Validation URL must be on the same host as the webhook URL")) + return + } + } + // Update integration metadata err = dbclient.Client.CustomIntegrations.Update(database.CustomIntegration{ Id: integration.Id, diff --git a/frontend/src/views/integrations/Activate.svelte b/frontend/src/views/integrations/Activate.svelte index 0229dde..663b058 100644 --- a/frontend/src/views/integrations/Activate.svelte +++ b/frontend/src/views/integrations/Activate.svelte @@ -79,7 +79,11 @@ let res = await axios.post(`${API_URL}/api/${guildId}/integrations/${integrationId}`, data); if (res.status !== 204) { - notifyError(res.data.error); + if (res.data.client_error) { + notifyError(`${res.data.error}: ${res.data.client_error}`); + } else { + notifyError(res.data.error); + } return; }