Skip to content

fix: Tighten share-URL validation against malicious color strings#60

Merged
Itroun merged 1 commit intostagingfrom
fix/share-url-validation
Apr 23, 2026
Merged

fix: Tighten share-URL validation against malicious color strings#60
Itroun merged 1 commit intostagingfrom
fix/share-url-validation

Conversation

@Itroun
Copy link
Copy Markdown
Contributor

@Itroun Itroun commented Apr 23, 2026

Summary

  • validateShareData previously only checked version === 1 and truthiness of grid/colors. A crafted share URL could set patternColors to arbitrary attacker-controlled strings, which then flow into exportSvg as fill=\"\${color}\" via string concatenation — a payload like \"/><script>...</script> in a downloaded SVG would execute when the victim opens the file directly in a browser.
  • Strict-validate share data at the boundary: hex-regex each entry in colors.pattern, colors.background, and palette.custom; require plain objects and finite numeric dimensions; require 2D-array shape for cells.
  • Run sanitizeGrid on share-URL cells to match the existing file-import path; promoted sanitizeGrid to an exported helper. Extracted HEX_COLOR_PATTERN to validation.js as the single source of truth.

Test plan

  • npm test — 193/193 unit tests pass (4 new cases for non-hex colors, bad dims, malformed cells, bad palette)
  • Smoke check: loading a normal share URL still works
  • Smoke check: loading a tampered share URL (hex regex violation) is rejected with the existing user-facing error

`validateShareData` previously only checked `version === 1` and the
truthiness of `grid`/`colors`, so a crafted share URL could set
`patternColors` to arbitrary strings. Those strings flow into
`exportSvg` as `fill="${color}"` via string concatenation, so a payload
like `"/><script>...</script>` in a downloaded SVG would execute when
the victim opens the file directly in a browser.

Strict-validate share data at the boundary: hex-regex each color in
`colors.pattern`, `colors.background`, and `palette.custom`; require
plain objects and finite numeric dimensions; require array-of-arrays
shape for cells. Also run `sanitizeGrid` on share-URL cells to match
the file-import path (export.js:817). Promoted `sanitizeGrid` to an
exported helper.
@Itroun Itroun merged commit fcabc53 into staging Apr 23, 2026
2 checks passed
Itroun added a commit that referenced this pull request Apr 23, 2026
* deps(deps-dev): Bump the patch-updates group with 2 updates (#52)

Bumps the patch-updates group with 2 updates: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `vite` from 8.0.4 to 8.0.8
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.8/packages/vite)

Updates `vitest` from 4.1.2 to 4.1.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.4/packages/vitest)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: vitest
  dependency-version: 4.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix: Deploy directly in dependabot workflow to bypass GITHUB_TOKEN limitation

GITHUB_TOKEN merges don't trigger other workflows, so deploy-production.yml
never fires after dependabot PRs are merged to main. The dependabot deploy
workflow now builds and deploys to Cloudflare Pages directly after merging,
and syncs main back into staging. Also adds workflow_dispatch to
deploy-production.yml for manual triggers.

* deps(deps-dev): Bump vite from 8.0.8 to 8.0.9 in the patch-updates group (#55)

Bumps the patch-updates group with 1 update: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 8.0.8 to 8.0.9
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.9/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* deps(deps-dev): Bump happy-dom from 20.8.9 to 20.9.0 (#56)

Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 20.8.9 to 20.9.0.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](capricorn86/happy-dom@v20.8.9...v20.9.0)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-version: 20.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix: Harden dependabot auto-deploy against back-to-back PRs (#59)

Back-to-back dependabot PRs landing on staging could race the
dependabot-deploy workflow: the second staging→main PR would enable
`gh pr merge --auto`, which silently no-ops when no required check is
pending, leaving the PR stuck and the workflow timing out.

Three defenses:
- Group minor updates alongside patches so a typical week produces one
  combined PR instead of several.
- Try an immediate squash merge first and only fall back to --auto when
  the PR isn't yet mergeable, so --auto's no-op behavior can't strand
  a ready PR.
- Add a concurrency group so overlapping deploy runs queue instead of
  racing each other.

* fix: Tighten share-URL validation against malicious color strings (#60)

`validateShareData` previously only checked `version === 1` and the
truthiness of `grid`/`colors`, so a crafted share URL could set
`patternColors` to arbitrary strings. Those strings flow into
`exportSvg` as `fill="${color}"` via string concatenation, so a payload
like `"/><script>...</script>` in a downloaded SVG would execute when
the victim opens the file directly in a browser.

Strict-validate share data at the boundary: hex-regex each color in
`colors.pattern`, `colors.background`, and `palette.custom`; require
plain objects and finite numeric dimensions; require array-of-arrays
shape for cells. Also run `sanitizeGrid` on share-URL cells to match
the file-import path (export.js:817). Promoted `sanitizeGrid` to an
exported helper.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
@Itroun Itroun deleted the fix/share-url-validation branch April 23, 2026 13:09
github-actions Bot added a commit that referenced this pull request Apr 27, 2026
* deps(deps-dev): Bump the patch-updates group with 2 updates (#52)

Bumps the patch-updates group with 2 updates: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `vite` from 8.0.4 to 8.0.8
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.8/packages/vite)

Updates `vitest` from 4.1.2 to 4.1.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.4/packages/vitest)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: vitest
  dependency-version: 4.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix: Deploy directly in dependabot workflow to bypass GITHUB_TOKEN limitation

GITHUB_TOKEN merges don't trigger other workflows, so deploy-production.yml
never fires after dependabot PRs are merged to main. The dependabot deploy
workflow now builds and deploys to Cloudflare Pages directly after merging,
and syncs main back into staging. Also adds workflow_dispatch to
deploy-production.yml for manual triggers.

* deps(deps-dev): Bump vite from 8.0.8 to 8.0.9 in the patch-updates group (#55)

Bumps the patch-updates group with 1 update: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 8.0.8 to 8.0.9
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.9/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* deps(deps-dev): Bump happy-dom from 20.8.9 to 20.9.0 (#56)

Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 20.8.9 to 20.9.0.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](capricorn86/happy-dom@v20.8.9...v20.9.0)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-version: 20.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix: Harden dependabot auto-deploy against back-to-back PRs (#59)

Back-to-back dependabot PRs landing on staging could race the
dependabot-deploy workflow: the second staging→main PR would enable
`gh pr merge --auto`, which silently no-ops when no required check is
pending, leaving the PR stuck and the workflow timing out.

Three defenses:
- Group minor updates alongside patches so a typical week produces one
  combined PR instead of several.
- Try an immediate squash merge first and only fall back to --auto when
  the PR isn't yet mergeable, so --auto's no-op behavior can't strand
  a ready PR.
- Add a concurrency group so overlapping deploy runs queue instead of
  racing each other.

* fix: Tighten share-URL validation against malicious color strings (#60)

`validateShareData` previously only checked `version === 1` and the
truthiness of `grid`/`colors`, so a crafted share URL could set
`patternColors` to arbitrary strings. Those strings flow into
`exportSvg` as `fill="${color}"` via string concatenation, so a payload
like `"/><script>...</script>` in a downloaded SVG would execute when
the victim opens the file directly in a browser.

Strict-validate share data at the boundary: hex-regex each color in
`colors.pattern`, `colors.background`, and `palette.custom`; require
plain objects and finite numeric dimensions; require array-of-arrays
shape for cells. Also run `sanitizeGrid` on share-URL cells to match
the file-import path (export.js:817). Promoted `sanitizeGrid` to an
exported helper.

* deps(deps-dev): Bump the patch-updates group with 2 updates (#62)

Bumps the patch-updates group with 2 updates: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `vite` from 8.0.9 to 8.0.10
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.10/packages/vite)

Updates `vitest` from 4.1.4 to 4.1.5
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.5/packages/vitest)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: vitest
  dependency-version: 4.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Itroun <78351901+Itroun@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Itroun added a commit that referenced this pull request Apr 27, 2026
* deps(deps-dev): Bump the patch-updates group with 2 updates (#52)

Bumps the patch-updates group with 2 updates: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `vite` from 8.0.4 to 8.0.8
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.8/packages/vite)

Updates `vitest` from 4.1.2 to 4.1.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.4/packages/vitest)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: vitest
  dependency-version: 4.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix: Deploy directly in dependabot workflow to bypass GITHUB_TOKEN limitation

GITHUB_TOKEN merges don't trigger other workflows, so deploy-production.yml
never fires after dependabot PRs are merged to main. The dependabot deploy
workflow now builds and deploys to Cloudflare Pages directly after merging,
and syncs main back into staging. Also adds workflow_dispatch to
deploy-production.yml for manual triggers.

* deps(deps-dev): Bump vite from 8.0.8 to 8.0.9 in the patch-updates group (#55)

Bumps the patch-updates group with 1 update: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 8.0.8 to 8.0.9
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.9/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* deps(deps-dev): Bump happy-dom from 20.8.9 to 20.9.0 (#56)

Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 20.8.9 to 20.9.0.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](capricorn86/happy-dom@v20.8.9...v20.9.0)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-version: 20.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix: Harden dependabot auto-deploy against back-to-back PRs (#59)

Back-to-back dependabot PRs landing on staging could race the
dependabot-deploy workflow: the second staging→main PR would enable
`gh pr merge --auto`, which silently no-ops when no required check is
pending, leaving the PR stuck and the workflow timing out.

Three defenses:
- Group minor updates alongside patches so a typical week produces one
  combined PR instead of several.
- Try an immediate squash merge first and only fall back to --auto when
  the PR isn't yet mergeable, so --auto's no-op behavior can't strand
  a ready PR.
- Add a concurrency group so overlapping deploy runs queue instead of
  racing each other.

* fix: Tighten share-URL validation against malicious color strings (#60)

`validateShareData` previously only checked `version === 1` and the
truthiness of `grid`/`colors`, so a crafted share URL could set
`patternColors` to arbitrary strings. Those strings flow into
`exportSvg` as `fill="${color}"` via string concatenation, so a payload
like `"/><script>...</script>` in a downloaded SVG would execute when
the victim opens the file directly in a browser.

Strict-validate share data at the boundary: hex-regex each color in
`colors.pattern`, `colors.background`, and `palette.custom`; require
plain objects and finite numeric dimensions; require array-of-arrays
shape for cells. Also run `sanitizeGrid` on share-URL cells to match
the file-import path (export.js:817). Promoted `sanitizeGrid` to an
exported helper.

* deps(deps-dev): Bump the patch-updates group with 2 updates (#62)

Bumps the patch-updates group with 2 updates: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `vite` from 8.0.9 to 8.0.10
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.10/packages/vite)

Updates `vitest` from 4.1.4 to 4.1.5
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.5/packages/vitest)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: vitest
  dependency-version: 4.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix: Fetch full history when checking out main for back-merge (#64)

The dependabot-deploy workflow's post-deploy "Sync main into staging"
step was failing with "refusing to merge unrelated histories" because
the preceding `Checkout main (post-merge)` step used the default
shallow clone (depth=1). With no shared history fetched, git cannot
find a common ancestor between origin/main and origin/staging and
refuses the merge.

Set fetch-depth: 0 on that checkout so the back-merge succeeds.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant