From ed81ca760f5e068ba24dc2f33a39e78d7a65cc9e Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 18 Feb 2026 11:21:25 -0800 Subject: [PATCH] Restore unique index on board_publications.key (#2568) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Restore unique index on board_publications.key The account_id rollout (fe6df7085) replaced the unique index on board_publications.key with a non-unique composite (account_id, key) index. This was unintentional — other tables in the same migration preserved uniqueness (account_join_codes, tags) but this one did not. Without global uniqueness on key, a crafted data import can insert a duplicate publication key via insert_all! (which bypasses model validations). Since find_by_published_key queries globally without account scoping, this enables public board URL hijacking after the legitimate owner unpublishes. Restore the original unique index on key and keep a plain account_id index for account-scoped queries. * Reorder index operations: add unique index before dropping composite On MySQL, DDL is non-transactional. If the unique index creation fails (e.g. duplicate keys exist), the previous ordering would leave the composite index already dropped. Adding the unique index first means a failure leaves the database unchanged. * Add schema dumps for both SQLite and MySQL Dump both schema files after running the migration against each adapter to keep the PR complete for CI. --- ...120000_restore_unique_index_on_board_publication_key.rb | 7 +++++++ db/schema.rb | 5 +++-- db/schema_sqlite.rb | 5 +++-- 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20260218120000_restore_unique_index_on_board_publication_key.rb diff --git a/db/migrate/20260218120000_restore_unique_index_on_board_publication_key.rb b/db/migrate/20260218120000_restore_unique_index_on_board_publication_key.rb new file mode 100644 index 000000000..dfcd5edaf --- /dev/null +++ b/db/migrate/20260218120000_restore_unique_index_on_board_publication_key.rb @@ -0,0 +1,7 @@ +class RestoreUniqueIndexOnBoardPublicationKey < ActiveRecord::Migration[8.2] + def change + add_index :board_publications, :key, unique: true + add_index :board_publications, :account_id + remove_index :board_publications, [:account_id, :key] + end +end diff --git a/db/schema.rb b/db/schema.rb index e74a97212..20cb0d161 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.2].define(version: 2026_02_13_170100) do +ActiveRecord::Schema[8.2].define(version: 2026_02_18_120000) do create_table "accesses", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.datetime "accessed_at" t.uuid "account_id", null: false @@ -147,8 +147,9 @@ ActiveRecord::Schema[8.2].define(version: 2026_02_13_170100) do t.datetime "created_at", null: false t.string "key" t.datetime "updated_at", null: false - t.index ["account_id", "key"], name: "index_board_publications_on_account_id_and_key" + t.index ["account_id"], name: "index_board_publications_on_account_id" t.index ["board_id"], name: "index_board_publications_on_board_id" + t.index ["key"], name: "index_board_publications_on_key", unique: true end create_table "boards", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| diff --git a/db/schema_sqlite.rb b/db/schema_sqlite.rb index e06492288..c8ce19e07 100644 --- a/db/schema_sqlite.rb +++ b/db/schema_sqlite.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.2].define(version: 2026_02_13_170100) do +ActiveRecord::Schema[8.2].define(version: 2026_02_18_120000) do create_table "accesses", id: :uuid, force: :cascade do |t| t.datetime "accessed_at" t.uuid "account_id", null: false @@ -147,8 +147,9 @@ ActiveRecord::Schema[8.2].define(version: 2026_02_13_170100) do t.datetime "created_at", null: false t.string "key", limit: 255 t.datetime "updated_at", null: false - t.index ["account_id", "key"], name: "index_board_publications_on_account_id_and_key" + t.index ["account_id"], name: "index_board_publications_on_account_id" t.index ["board_id"], name: "index_board_publications_on_board_id" + t.index ["key"], name: "index_board_publications_on_key", unique: true end create_table "boards", id: :uuid, force: :cascade do |t|