diff --git a/.opencode/skills/update-changelog/SKILL.md b/.opencode/skills/update-changelog/SKILL.md index d5c0ccf2af..d456ce5bf9 100644 --- a/.opencode/skills/update-changelog/SKILL.md +++ b/.opencode/skills/update-changelog/SKILL.md @@ -212,6 +212,37 @@ superseded it: Replace the reference in the changelog entry with the correct merged PR number. +### 5b. Security advisory (GHSA) entries + +Security advisories fixed in a release are documented in the changelog even +though they are **neither milestone issues nor PRs**. The GHSA ID and its +description are supplied by the user or the release notes — they never come +from the milestone fetch in step 2. + +**Format** (matches the existing precedent in `CHANGES.md`, e.g. the +`create-font-variant` arbitrary file read advisory): + +```markdown +- Fix (https://github.com/penpot/penpot/security/advisories/GHSA-XXXX-XXXX-XXXX) +``` + +Rules: +- Place the entry under `### :bug: Bugs fixed`, with **no issue or PR link** — + only the advisory URL. +- The advisory may be **draft/unpublished** at changelog time (the URL 404s + publicly). Do **not** web-fetch or verify the URL, and do **not** drop the + entry because of that. Rely on the GHSA ID provided by the user. +- Derive the description from the supplied advisory title, imperative mood and + user-facing (e.g. `Fix command injection in SVG exporter via legacy fill-color`). +- These entries are **invisible to the automation**: they are not returned by + `gh.py issues`, not matched by `--compare` (step 3), not part of the PR + cross-reference (step 10), and not scanned by the anomaly-report regexes + (step 11, which only match `issues/` and `pull/` links). Add them manually. +- During pre-flight checks (step 6a) apply only the **backport/duplicate** + check: if the same GHSA already appears in an earlier version section, remove + it from the current section. Their absence from milestone cross-references + is expected, not an anomaly. + ### 6. Read the current CHANGES.md Read the top of `CHANGES.md` to understand the existing format and find the @@ -400,6 +431,8 @@ if closed: - ✅ Every merged milestone PR is either in the changelog or excluded by label - ✅ PR and issue counts are internally consistent - ✅ No false-positive PR-to-issue associations +- ✅ Advisory (GHSA) entries are not milestone PRs — their absence from the + cross-reference is intentional (see step 5b) ## Version section template @@ -410,8 +443,12 @@ if closed: - [#](https://github.com/penpot/penpot/issues/) (PR: [#](https://github.com/penpot/penpot/pull/)) - (by @contributor) [#](https://github.com/penpot/penpot/issues/) (PR: [#](https://github.com/penpot/penpot/pull/)) +- (https://github.com/penpot/penpot/security/advisories/GHSA-XXXX-XXXX-XXXX) ``` +Advisory (GHSA) entries have no issue or PR link — just the advisory URL. See +step 5b. + ### 11. Generate anomaly report and save to CHANGES-ISSUES.md After all edits and cross-referencing are complete, generate a structured @@ -732,6 +769,14 @@ self-contained and clickable in any Markdown viewer. Taiga description text or by searching GitHub PRs that reference the Taiga URL. Replace the Taiga reference with the GitHub issue link and add the PR reference if applicable. +- **Security advisory (GHSA) entries.** Advisories fixed in the release are + listed under `### :bug: Bugs fixed` with the advisory URL and **no issue or + PR link**, even though they are not in the milestone. The GHSA ID and + description come from the user — do **not** fetch or verify the URL, and do + not drop a draft (unpublished) advisory. Precedent: + `- Fix arbitrary file read security issue on create-font-variant rpc method + (https://github.com/penpot/penpot/security/advisories/GHSA-xp3f-g8rq-9px2)`. + See step 5b. - **Re-fetch before editing.** Milestones can change — always re-fetch issues before making edits, don't rely on cached data. - **Use `scripts/gh.py`.** Prefer the helper script over raw `gh api` calls for diff --git a/CHANGES.md b/CHANGES.md index d629c16756..a414495e33 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,13 @@ - Refactor wasm rulers and UI state [#10116](https://github.com/penpot/penpot/issues/10116) (PR: [#10461](https://github.com/penpot/penpot/pull/10461)) - Improve team invitations modal in the dashboard [#10484](https://github.com/penpot/penpot/issues/10484) (PR: [#10459](https://github.com/penpot/penpot/pull/10459)) +## 2.17.2 + +### :bug: Bugs fixed + +- Fix linear gradients in SVG text exports being emitted as radial gradients [#5972](https://github.com/penpot/penpot/issues/5972) (PR: [#11272](https://github.com/penpot/penpot/pull/11272)) +- Fix typography token becoming detached when editing text content [#11362](https://github.com/penpot/penpot/issues/11362) (PR: [#11366](https://github.com/penpot/penpot/pull/11366)) +- Fix command injection in SVG exporter via legacy fill-color (https://github.com/penpot/penpot/security/advisories/GHSA-4f36-m4hj-cv86) ## 2.17.1 diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc index f59f09ef50..de8d1b8f96 100644 --- a/common/src/app/common/files/migrations.cljc +++ b/common/src/app/common/files/migrations.cljc @@ -1978,6 +1978,26 @@ (update :pages-index d/update-vals update-container) (d/update-when :components d/update-vals update-container)))) +(defmethod migrate-data "0026-fix-svg-raw-shapes-uuids" + ;; Before the svg-raw schema declared :shapes as a vector of uuid, + ;; the JSON decoder had no type information for those child ids and + ;; left them as plain strings on any round trip, so they got + ;; persisted as strings. Once the schema was tightened, such files + ;; fail schema validation; this migration parses the strings back + ;; into uuid instances. + [data _] + (letfn [(update-object [object] + (cond-> object + (cfh/svg-raw-shape? object) + (d/update-when :shapes #(mapv uuid/coerce %)))) + + (update-container [container] + (d/update-when container :objects d/update-vals update-object))] + + (-> data + (update :pages-index d/update-vals update-container) + (d/update-when :components d/update-vals update-container)))) + (def available-migrations (into (d/ordered-set) ["legacy-2" @@ -2060,4 +2080,5 @@ "0022-normalize-component-root-and-resync" "0023-repair-token-themes-with-inexistent-sets" "0024b-fix-stroke-cap-placement" - "0025-repair-empty-text-content"])) + "0025-repair-empty-text-content" + "0026-fix-svg-raw-shapes-uuids"])) diff --git a/common/src/app/common/types/text.cljc b/common/src/app/common/types/text.cljc index 86fa8496cc..f7aeb37664 100644 --- a/common/src/app/common/types/text.cljc +++ b/common/src/app/common/types/text.cljc @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.flags :as flags] + [app.common.math :as mth] [app.common.types.color :as clr] [app.common.types.fills :as types.fills] [clojure.set :as set] @@ -217,7 +218,10 @@ attributes or other things that may be attached). - Consider nil values, empty strings or empty lists all equal. - Normalize numeric values (legacy) into strings. - - No value is equal than the default value." + - No value is equal than the default value. + - Numeric attrs (e.g. line-height) compare with float tolerance so + editor/WASM round-trips like \"1.3333333333333333\" vs \"1.33333\" + do not count as a real style change (avoids detaching tokens)." [key value1 value2] (when (text-node-attr? key) (let [default-value (get default-text-attrs key) @@ -229,7 +233,16 @@ $))) value1' (normalize-value value1) value2' (normalize-value value2)] - (not= value1' value2')))) + (cond + (= value1' value2') + false + + :else + (let [n1 (when (string? value1') (d/parse-double value1')) + n2 (when (string? value2') (d/parse-double value2'))] + (if (and (some? n1) (some? n2)) + (not (mth/close? n1 n2)) + true)))))) (defn- compare-text-content "Given two content text structures, conformed by maps and vectors, diff --git a/common/test/common_tests/files_migrations_0026_test.cljc b/common/test/common_tests/files_migrations_0026_test.cljc new file mode 100644 index 0000000000..92dde088e8 --- /dev/null +++ b/common/test/common_tests/files_migrations_0026_test.cljc @@ -0,0 +1,110 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC Sucursal en España SL + +(ns common-tests.files-migrations-0026-test + (:require + [app.common.files.migrations :as cfm] + [app.common.uuid :as uuid] + [clojure.test :as t])) + +;; 0026-fix-svg-raw-shapes-uuids +;; Before the svg-raw schema declared :shapes as a vector of uuid, the +;; JSON decoder had no type information for those child ids and left +;; them as plain strings on any round trip, so they got persisted as +;; strings. Once the schema was tightened, such files fail schema +;; validation; this migration parses the strings back into uuids. + +(defn- make-svg-raw-shape + "Build a minimal svg-raw shape with the supplied :shapes vector. + When `shapes` is nil the :shapes key is omitted, like a leaf svg-raw + shape." + [shape-id shapes] + (cond-> {:id shape-id + :type :svg-raw} + (some? shapes) + (assoc :shapes shapes))) + +(defn- make-other-shape + "Build a minimal non-svg-raw shape that must stay untouched." + [shape-id shapes] + {:id shape-id + :type :group + :shapes shapes}) + +(t/deftest migration-0026-converts-svg-raw-shapes-strings-to-uuids-in-pages + (let [shape-id (uuid/next) + child-id (uuid/next) + page-id (uuid/next) + data {:pages-index + {page-id + {:objects + {shape-id (make-svg-raw-shape + shape-id + [(str child-id) + "1c2986ce-4a0f-8001-8007-1fb8f3b5ab31"])}}}} + data' (cfm/migrate-data data "0026-fix-svg-raw-shapes-uuids") + shape (get-in data' [:pages-index page-id :objects shape-id])] + + (t/is (= 2 (count (:shapes shape))) "child ids preserved") + (t/is (= child-id (first (:shapes shape))) "existing uuid string parsed to uuid") + (t/is (= #uuid "1c2986ce-4a0f-8001-8007-1fb8f3b5ab31" (second (:shapes shape))) + "foreign uuid string parsed to uuid") + (t/is (every? uuid? (:shapes shape)) "all child ids are uuids"))) + +(t/deftest migration-0026-converts-svg-raw-shapes-strings-to-uuids-in-components + (let [shape-id (uuid/next) + child-id (uuid/next) + component-id (uuid/next) + data {:components + {component-id + {:objects + {shape-id (make-svg-raw-shape + shape-id + [(str child-id) + "1c2986ce-4a0f-8001-8007-1fb92196e65f"])}}}} + data' (cfm/migrate-data data "0026-fix-svg-raw-shapes-uuids") + shape (get-in data' [:components component-id :objects shape-id])] + + (t/is (= 2 (count (:shapes shape))) "child ids preserved") + (t/is (= child-id (first (:shapes shape))) "existing uuid string parsed to uuid") + (t/is (= #uuid "1c2986ce-4a0f-8001-8007-1fb92196e65f" (second (:shapes shape))) + "foreign uuid string parsed to uuid") + (t/is (every? uuid? (:shapes shape)) "all child ids are uuids"))) + +(t/deftest migration-0026-leaves-uuids-and-other-shapes-untouched + (let [svg-raw-id (uuid/next) + child-id (uuid/next) + group-id (uuid/next) + leaf-id (uuid/next) + page-id (uuid/next) + data {:pages-index + {page-id + {:objects + {svg-raw-id (make-svg-raw-shape svg-raw-id [child-id]) + group-id (make-other-shape group-id [(str child-id)]) + leaf-id (make-svg-raw-shape leaf-id nil)}}}} + data' (cfm/migrate-data data "0026-fix-svg-raw-shapes-uuids") + objects (get-in data' [:pages-index page-id :objects])] + + (t/is (= [child-id] (:shapes (get objects svg-raw-id))) + "already-uuid svg-raw children untouched") + (t/is (= [(str child-id)] (:shapes (get objects group-id))) + "non-svg-raw shapes untouched") + (t/is (nil? (:shapes (get objects leaf-id))) + "svg-raw leaf without :shapes untouched"))) + +(t/deftest migration-0026-is-idempotent + (let [shape-id (uuid/next) + child-id (uuid/next) + page-id (uuid/next) + data {:pages-index + {page-id + {:objects + {shape-id (make-svg-raw-shape shape-id [(str child-id)])}}}} + data' (cfm/migrate-data data "0026-fix-svg-raw-shapes-uuids") + data'' (cfm/migrate-data data' "0026-fix-svg-raw-shapes-uuids")] + + (t/is (= data' data'') "second run is a no-op"))) \ No newline at end of file diff --git a/common/test/common_tests/runner.cljc b/common/test/common_tests/runner.cljc index 04232089ab..db0dde0bbb 100644 --- a/common/test/common_tests/runner.cljc +++ b/common/test/common_tests/runner.cljc @@ -19,6 +19,7 @@ [common-tests.files-builder-test] [common-tests.files-changes-test] [common-tests.files-migrations-0025-test] + [common-tests.files-migrations-0026-test] [common-tests.files-migrations-test] [common-tests.files.shapes-builder-test] [common-tests.files.validate-test] @@ -97,6 +98,7 @@ 'common-tests.files-changes-test 'common-tests.files-builder-test 'common-tests.files-migrations-0025-test + 'common-tests.files-migrations-0026-test 'common-tests.files-migrations-test 'common-tests.files.validate-test 'common-tests.geom-align-test diff --git a/common/test/common_tests/types/text_test.cljc b/common/test/common_tests/types/text_test.cljc index 6fa69360f3..8165558bed 100644 --- a/common/test/common_tests/types/text_test.cljc +++ b/common/test/common_tests/types/text_test.cljc @@ -78,6 +78,14 @@ (def content-changed-line-height (assoc-in content-base [:children 0 :children 0 :line-height] "1.5")) +;; Token/WASM may store full float precision; editor round-trips often +;; truncate (e.g. CSS / f32). These must compare as equal. +(def content-line-height-full-precision + (assoc-in content-base [:children 0 :children 0 :line-height] "1.3333333333333333")) + +(def content-line-height-truncated + (assoc-in content-base [:children 0 :children 0 :line-height] "1.33333")) + (def content-redundant-span-line-height (assoc-in content-base [:children 0 :children 0 :children 0 :line-height] "1.5")) @@ -208,6 +216,8 @@ ;; Other text-node-attr categories attrs-font-family (cttx/get-diff-attrs content-base content-changed-font-family) attrs-line-height (cttx/get-diff-attrs content-base content-changed-line-height) + attrs-line-height-precision (cttx/get-diff-attrs content-line-height-full-precision + content-line-height-truncated) attrs-span-line-height (cttx/get-diff-attrs content-base content-redundant-span-line-height) attrs-roundtrip-line-height (cttx/get-diff-attrs content-token-like-line-height content-after-editor-roundtrip) @@ -242,6 +252,7 @@ ;; Each text-node-attr category reports correct attr key (t/is (= #{:font-family} attrs-font-family)) (t/is (= #{:line-height} attrs-line-height)) + (t/is (= #{} attrs-line-height-precision)) (t/is (= #{} attrs-span-line-height)) (t/is (= #{} attrs-roundtrip-line-height)) (t/is (= #{} attrs-nil-typography-refs))