From 1cc10f5fffca7155c48e03ce406bf9fcbd87b52d Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Thu, 4 Jun 2026 23:30:55 -0400 Subject: [PATCH] fix: Increase valid user-supplied URL length to 1024 (#12585) * fix: Increase valid user-supplied URL length to 1024 * fix: Wrap URL length migration in a transaction Wrap the multi-column changeColumn operations in a transaction so a failure on any column rolls back the whole migration rather than leaving the database partially migrated. Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- .../components/WebhookSubscriptionForm.tsx | 7 +- plugins/webhooks/server/api/schema.ts | 4 + ...60604140753-increase-url-column-lengths.js | 106 ++++++++++++++++++ shared/validations.ts | 8 +- 4 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 server/migrations/20260604140753-increase-url-column-lengths.js diff --git a/plugins/webhooks/client/components/WebhookSubscriptionForm.tsx b/plugins/webhooks/client/components/WebhookSubscriptionForm.tsx index 61d0fc24fd..74295b2b00 100644 --- a/plugins/webhooks/client/components/WebhookSubscriptionForm.tsx +++ b/plugins/webhooks/client/components/WebhookSubscriptionForm.tsx @@ -7,6 +7,7 @@ import { useTranslation, Trans } from "react-i18next"; import styled from "styled-components"; import { randomString } from "@shared/random"; import { TeamPreference } from "@shared/types"; +import { WebhookSubscriptionValidation } from "@shared/validations"; import type WebhookSubscription from "~/models/WebhookSubscription"; import Button from "~/components/Button"; import Input from "~/components/Input"; @@ -229,6 +230,7 @@ function WebhookSubscriptionForm({ handleSubmit, webhookSubscription }: Props) { required flex pattern={isCloudHosted ? "https://.*" : "https?://.*"} + maxLength={WebhookSubscriptionValidation.maxUrlLength} placeholder="https://…" label={t("URL")} error={ @@ -238,7 +240,10 @@ function WebhookSubscriptionForm({ handleSubmit, webhookSubscription }: Props) { ) : undefined } - {...register("url", { required: true })} + {...register("url", { + required: true, + maxLength: WebhookSubscriptionValidation.maxUrlLength, + })} /> !env.isCloudHosted || val.startsWith("https://"), { error: "Webhook url must use https", }); diff --git a/server/migrations/20260604140753-increase-url-column-lengths.js b/server/migrations/20260604140753-increase-url-column-lengths.js new file mode 100644 index 0000000000..450c6bda22 --- /dev/null +++ b/server/migrations/20260604140753-increase-url-column-lengths.js @@ -0,0 +1,106 @@ +"use strict"; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.changeColumn( + "webhook_subscriptions", + "url", + { + type: Sequelize.STRING(1024), + allowNull: false, + }, + { transaction } + ); + await queryInterface.changeColumn( + "oauth_clients", + "developerUrl", + { + type: Sequelize.STRING(1024), + allowNull: true, + }, + { transaction } + ); + await queryInterface.changeColumn( + "oauth_clients", + "avatarUrl", + { + type: Sequelize.STRING(1024), + allowNull: true, + }, + { transaction } + ); + await queryInterface.changeColumn( + "oauth_clients", + "redirectUris", + { + type: Sequelize.ARRAY(Sequelize.STRING(1024)), + allowNull: false, + defaultValue: [], + }, + { transaction } + ); + await queryInterface.changeColumn( + "oauth_authorization_codes", + "redirectUri", + { + type: Sequelize.STRING(1024), + allowNull: false, + }, + { transaction } + ); + }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.changeColumn( + "oauth_authorization_codes", + "redirectUri", + { + type: Sequelize.STRING(255), + allowNull: false, + }, + { transaction } + ); + await queryInterface.changeColumn( + "oauth_clients", + "redirectUris", + { + type: Sequelize.ARRAY(Sequelize.STRING(255)), + allowNull: false, + defaultValue: [], + }, + { transaction } + ); + await queryInterface.changeColumn( + "oauth_clients", + "avatarUrl", + { + type: Sequelize.STRING(255), + allowNull: true, + }, + { transaction } + ); + await queryInterface.changeColumn( + "oauth_clients", + "developerUrl", + { + type: Sequelize.STRING(255), + allowNull: true, + }, + { transaction } + ); + await queryInterface.changeColumn( + "webhook_subscriptions", + "url", + { + type: Sequelize.STRING(255), + allowNull: false, + }, + { transaction } + ); + }); + }, +}; diff --git a/shared/validations.ts b/shared/validations.ts index d97da1b1e7..612f6cc26f 100644 --- a/shared/validations.ts +++ b/shared/validations.ts @@ -89,13 +89,13 @@ export const OAuthClientValidation = { maxDeveloperNameLength: 100, /** The maximum length of the OAuth client developer URL */ - maxDeveloperUrlLength: 255, + maxDeveloperUrlLength: 1024, /** The maximum length of the OAuth client avatar URL */ - maxAvatarUrlLength: 255, + maxAvatarUrlLength: 1024, /** The maximum length of an OAuth client redirect URI */ - maxRedirectUriLength: 255, + maxRedirectUriLength: 1024, /** The allowed OAuth client types */ clientTypes: ["confidential", "public"] as const, @@ -170,7 +170,7 @@ export const WebhookSubscriptionValidation = { /** The maximum length of the webhook name */ maxNameLength: 255, /** The maximum length of the webhook url */ - maxUrlLength: 255, + maxUrlLength: 1024, }; export const EmojiValidation = {