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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Moor
2026-06-04 23:30:55 -04:00
committed by GitHub
parent ce3d710888
commit 1cc10f5fff
4 changed files with 120 additions and 5 deletions
@@ -7,6 +7,7 @@ import { useTranslation, Trans } from "react-i18next";
import styled from "styled-components"; import styled from "styled-components";
import { randomString } from "@shared/random"; import { randomString } from "@shared/random";
import { TeamPreference } from "@shared/types"; import { TeamPreference } from "@shared/types";
import { WebhookSubscriptionValidation } from "@shared/validations";
import type WebhookSubscription from "~/models/WebhookSubscription"; import type WebhookSubscription from "~/models/WebhookSubscription";
import Button from "~/components/Button"; import Button from "~/components/Button";
import Input from "~/components/Input"; import Input from "~/components/Input";
@@ -229,6 +230,7 @@ function WebhookSubscriptionForm({ handleSubmit, webhookSubscription }: Props) {
required required
flex flex
pattern={isCloudHosted ? "https://.*" : "https?://.*"} pattern={isCloudHosted ? "https://.*" : "https?://.*"}
maxLength={WebhookSubscriptionValidation.maxUrlLength}
placeholder="https://…" placeholder="https://…"
label={t("URL")} label={t("URL")}
error={ error={
@@ -238,7 +240,10 @@ function WebhookSubscriptionForm({ handleSubmit, webhookSubscription }: Props) {
) )
: undefined : undefined
} }
{...register("url", { required: true })} {...register("url", {
required: true,
maxLength: WebhookSubscriptionValidation.maxUrlLength,
})}
/> />
<Input <Input
flex flex
+4
View File
@@ -1,10 +1,14 @@
import { z } from "zod"; import { z } from "zod";
import { WebhookSubscriptionValidation } from "@shared/validations";
import env from "@server/env"; import env from "@server/env";
import { WebhookSubscription } from "@server/models"; import { WebhookSubscription } from "@server/models";
import { BaseSchema } from "@server/routes/api/schema"; import { BaseSchema } from "@server/routes/api/schema";
const webhookUrl = z const webhookUrl = z
.url() .url()
.max(WebhookSubscriptionValidation.maxUrlLength, {
error: `Webhook url must be ${WebhookSubscriptionValidation.maxUrlLength} characters or less`,
})
.refine((val) => !env.isCloudHosted || val.startsWith("https://"), { .refine((val) => !env.isCloudHosted || val.startsWith("https://"), {
error: "Webhook url must use https", error: "Webhook url must use https",
}); });
@@ -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 }
);
});
},
};
+4 -4
View File
@@ -89,13 +89,13 @@ export const OAuthClientValidation = {
maxDeveloperNameLength: 100, maxDeveloperNameLength: 100,
/** The maximum length of the OAuth client developer URL */ /** The maximum length of the OAuth client developer URL */
maxDeveloperUrlLength: 255, maxDeveloperUrlLength: 1024,
/** The maximum length of the OAuth client avatar URL */ /** The maximum length of the OAuth client avatar URL */
maxAvatarUrlLength: 255, maxAvatarUrlLength: 1024,
/** The maximum length of an OAuth client redirect URI */ /** The maximum length of an OAuth client redirect URI */
maxRedirectUriLength: 255, maxRedirectUriLength: 1024,
/** The allowed OAuth client types */ /** The allowed OAuth client types */
clientTypes: ["confidential", "public"] as const, clientTypes: ["confidential", "public"] as const,
@@ -170,7 +170,7 @@ export const WebhookSubscriptionValidation = {
/** The maximum length of the webhook name */ /** The maximum length of the webhook name */
maxNameLength: 255, maxNameLength: 255,
/** The maximum length of the webhook url */ /** The maximum length of the webhook url */
maxUrlLength: 255, maxUrlLength: 1024,
}; };
export const EmojiValidation = { export const EmojiValidation = {