mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user