diff --git a/.cursor/commands/patch-redash-container.md b/.cursor/commands/patch-redash-container.md
new file mode 100644
index 0000000000..0b51401959
--- /dev/null
+++ b/.cursor/commands/patch-redash-container.md
@@ -0,0 +1,116 @@
+# patch-redash-container
+
+## Fetch scan results (CLI)
+
+Use **`AWS_PROFILE=dev`** and run **outside the Cursor sandbox** (full permissions), for example:
+
+```bash
+AWS_PROFILE=dev aws ecr describe-image-scan-findings \
+ --region ap-southeast-2 \
+ --repository-name redash \
+ --image-id imageDigest=sha256: \
+ --output json > scan.json
+```
+
+Console link pattern (replace digest as needed):
+
+`https://ap-southeast-2.console.aws.amazon.com/ecr/repositories/private/639989371409/redash/_/image/sha256:/details?region=ap-southeast-2`
+
+Parse severity counts from `imageScanFindings.findingSeverityCounts` and details from `imageScanFindings.enhancedFindings`.
+
+**Note:** The scan results will show the state of the **previous** image. After pushing a new image, allow 24 hours for Inspector to complete the scan before fetching new results.
+
+## Prioritize findings
+
+Address in this order:
+
+1. **CRITICAL** — e.g. recent advisories on **axios**, **dompurify**, **lodash**, **tough-cookie**, **flatted**, **elliptic** (GHSA), plus any **Python** packages flagged (e.g. **urllib3**, **flask**).
+2. **HIGH** — transitive JS (e.g. **babel-traverse**, **cross-spawn**, **path-to-regexp**, **tar**, **minimatch**, **qs**, **braces**, **serialize-javascript**) and Python deps as listed in findings.
+3. **MEDIUM/LOW** — Address if time permits, but prioritize CRITICAL and HIGH first.
+
+**Important notes:**
+- Re-check each CVE against the **declared fixed version** in the finding; some Inspector IDs (especially future-dated CVE years) should be **confirmed with vendor/OS** before over-pinning.
+- **OS-level vulnerabilities** (libxml2, postgresql, nghttp2, etc.) require base Docker image updates and cannot be fixed via package managers.
+- Check if vulnerabilities are in the **base image** by looking at the package manager type (OS, DPKG, APT) - these should be skipped unless updating the base image.
+
+## Image hygiene (reduces noise)
+
+- If the scan references **`/app/yarn.lock` and `/app/pnpm-lock.yaml`**, the image contains **both** lockfiles. Prefer **one** JS package manager in the final app layer so scanners do not double-count the same npm tree.
+- After **Python** dependency bumps, run **`poetry lock`** (and commit **`poetry.lock`**) so Docker `poetry install` matches **`pyproject.toml`**.
+- After **JS** changes, run **`yarn install`** (or refresh **`yarn.lock`** and **`viz-lib/yarn.lock`**) so the Docker frontend stage stays consistent.
+- Use **`resolutions`** field in `package.json` and `viz-lib/package.json` to force specific versions of transitive dependencies.
+
+You should prefer `pnpm` over `yarn` because that is now on the `master` branch in the upstream repo. See https://github.com/getredash/redash
+
+**Current branch uses Yarn** - this fork maintains Yarn for consistency with the v26.3.0 base.
+
+## Validate locally (Docker-first)
+
+1. **`make compose_build`** — must pass frontend (Yarn/webpack) and backend (Poetry) stages.
+2. **`make test`** — runs full test suite (backend + frontend + linting).
+ - Backend: ~887 tests (pytest)
+ - Frontend: ~89 tests (jest)
+ - Expected time: ~4 minutes
+ - Note: Some tests may be skipped (e.g., JWT tests that have environment issues in full suite but pass in isolation)
+3. **Check linter errors** — Pre-commit hooks run `black` and `ruff` for Python code formatting.
+
+## Git
+
+**Commit** remediation to git on a new branch for these fixes (not `master`); **do not push** until I have had time to manually test the image.
+
+**Commit message format:**
+```
+fix: update dependencies and resolve
+
+Brief description of what was updated and why.
+
+Python dependency updates:
+- package: old → new (reason/CVE)
+
+JavaScript dependency updates:
+- package: old → new (reason/CVE)
+
+Configuration changes:
+- Any settings or behavior changes
+```
+
+## Updating ECR
+
+When I instruct you to push to ECR, use these steps:
+
+**Prerequisites:**
+- Must run with `required_permissions: ["all"]` (outside sandbox)
+- Requires `AWS_PROFILE=dev` for ECR authentication
+- Docker build takes ~6 minutes for ARM64 platform
+- Docker push takes ~5 minutes (most layers cached after first push)
+
+```bash
+# 1. Create and push git tag
+export TAG_VERSION=v26.3.0p5 # Increment patch number
+git tag $TAG_VERSION
+git push origin $TAG_VERSION
+
+# 2. Build Docker image for ARM64
+docker build --platform linux/arm64 -t redash:$TAG_VERSION .
+
+# 3. Login to ECR (requires AWS_PROFILE=dev)
+AWS_PROFILE=dev aws ecr get-login-password --region ap-southeast-2 | \
+ docker login --username AWS --password-stdin 639989371409.dkr.ecr.ap-southeast-2.amazonaws.com
+
+# 4. Tag and push versioned image
+docker tag redash:$TAG_VERSION 639989371409.dkr.ecr.ap-southeast-2.amazonaws.com/redash:$TAG_VERSION
+docker push 639989371409.dkr.ecr.ap-southeast-2.amazonaws.com/redash:$TAG_VERSION
+
+# 5. Tag and push as latest
+docker tag redash:$TAG_VERSION 639989371409.dkr.ecr.ap-southeast-2.amazonaws.com/redash:latest
+docker push 639989371409.dkr.ecr.ap-southeast-2.amazonaws.com/redash:latest
+```
+
+**Verify push:**
+- Check digest matches between versioned tag and latest
+- Expected image size: ~1.89GB
+- Console: https://ap-southeast-2.console.aws.amazon.com/ecr/repositories/private/639989371409/redash
+
+**Latest versions:**
+- v26.3.0p4: sha256:7bc4028d5c84df5deb75a9e0480f093957025f6ccde6bc6a4dc1cc45bfdc08d2 (2026-05-15)
+
diff --git a/.cursor/commands/vulnerability-fix-summary.md b/.cursor/commands/vulnerability-fix-summary.md
new file mode 100644
index 0000000000..1ce8c70611
--- /dev/null
+++ b/.cursor/commands/vulnerability-fix-summary.md
@@ -0,0 +1,62 @@
+# Vulnerability Fix Summary
+
+## ✅ Fixed Vulnerabilities (This Round)
+
+### Python Dependencies
+- **jwcrypto**: 1.5.6 → 1.5.7 (CVE-2026-39373: JWE ZIP decompression bomb)
+
+### JavaScript Dependencies — Direct Changes
+- **markdown** → **marked** ^4.3.0 (GHSA-wx77-rp39-c6vg: ReDoS; no fix available for `markdown` package)
+- **elliptic**: removed (unused direct dependency; CVE-2025-14505 has no patched release)
+- **babel-plugin-transform-builtin-extend**: removed (eliminated `babel-traverse@6` / Babel 6 chain; CVE-2023-45133)
+- **request** / **request-cookies**: removed from `client/cypress/cypress.js`; replaced with **axios** (already a project dependency)
+- **babel-plugin-istanbul**: 6.1.1 → 8.0.0 (fixes Jest + `minimatch@10` override compatibility)
+- **core-js** ^2.6.12: added explicit devDependency (required by `@babel/preset-env` `useBuiltIns: "usage"` after removing babel-plugin-transform-builtin-extend)
+
+### JavaScript Dependencies — Yarn resolutions (root `package.json`)
+- **@babel/plugin-transform-modules-systemjs**: → ^7.29.4 (CVE-2026-44728)
+- **@babel/preset-env**: → ^7.29.5
+- **fast-uri**: → ^3.1.2 (CVE-2026-6321, CVE-2026-6322)
+- **postcss**: → ^8.5.10 (CVE-2026-41305, CVE-2023-44270)
+- **autoprefixer**: → ^10.4.20 (pulls patched postcss for less-plugin-autoprefix)
+- **webpack-dev-server**: → ^5.2.4 (CVE-2025-30359, CVE-2026-6402)
+- **@cypress/request**: → ^3.0.10 (GHSA-p8p7-x288-28g6 SSRF)
+- **request**: aliased to `npm:@cypress/request@^3.0.10` (removes `request@2.88.2` from Percy agent chain)
+
+## ⚠️ Remaining Vulnerabilities (Accepted / Requires Larger Migration)
+
+### Bootstrap 3.x (Moderate — #601, #602)
+- **Package**: `bootstrap@3.4.1`
+- **CVEs**: CVE-2019-8331 (data-* XSS), CVE-2025-1647 (popover/tooltip DOM clobbering XSS)
+- **Why not fixed**: Bootstrap 3 is EOL; CVE-2025-1647 has no open-source patch (only HeroDevs NES 3.4.7). Redash uses Bootstrap **only for Less/CSS** (grid, typography); tooltips/popovers use **Ant Design**, not Bootstrap JS.
+- **Mitigation**: Output from markdown widgets is sanitized via DOMPurify in `HtmlContent`.
+- **Long-term**: Migrate to Bootstrap 5 or remove Bootstrap CSS dependency.
+
+### Paramiko SHA-1 (Low — #705, #708)
+- **Package**: `paramiko@3.4.1`
+- **CVE**: CVE-2026-44405
+- **Why not fixed**: Fix requires Paramiko 5.0.0 (breaking: removes SHA-1 RSA). `sshtunnel@0.1.5` is unmaintained and incompatible with Paramiko 4+/5.0.
+- **Risk**: Low CVSS 3.4; only used for optional SSH tunnel to data sources.
+
+### OS/System Level (from prior round)
+- libxml2, postgresql-17, nghttp2 — require Docker base image updates.
+
+### PySAML2 (ECR / GitLab advisory)
+- **pysaml2 7.5.x** is not currently usable: it requires **`pyopenssl <24.3.0`**, which conflicts with Redash’s **`pyopenssl` 26.x**. Staying on **pysaml2 7.3.1**; ECR **GMS-2016-67** remains a known advisory with no simple dependency bump.
+
+## 🧪 Verification
+- Production webpack build: ✅
+- Frontend Jest: 89 passed, 1 skipped
+- viz-lib Jest: 149 passed
+- viz-lib Babel + webpack production build: ✅
+
+## viz-lib (ECR / lockfile — Node findings)
+
+- **`viz-lib/yarn.lock`** previously contained **`request@2.88.2`**, **`postcss@6/7`**, and **`node-notifier@5.4.5`** (Jest 24 chain), which drove Inspector findings on paths like `/app/viz-lib/yarn.lock`.
+- **Aligned with root-style resolutions** in `viz-lib/package.json`: `postcss@^8.5.10`, `request` → `@cypress/request@^3.0.10`, `node-notifier@^10.0.1`, `cheerio@1.0.0-rc.12`, plus shared pins (`form-data`, `tough-cookie`, `webpack`, etc.). **Did not** pin `minimatch@10` in viz-lib (breaks `babel-plugin-istanbul@6` + Jest 24 / `test-exclude`).
+- **Dev tooling**: `css-loader` **3 → 7**, `style-loader` **3 → 4** so PostCSS 8 is supported for viz-lib webpack builds.
+
+## 📝 Files Changed
+- `viz-lib/package.json`, `viz-lib/yarn.lock` (this round: ECR app-layer / lockfile alignment)
+- `.cursor/commands/vulnerability-fix-summary.md`
+- Earlier rounds on same branch: `package.json`, `yarn.lock`, `pyproject.toml`, `poetry.lock`, `client/.babelrc`, `client/cypress/cypress.js`, markdown widget components (`TextboxDialog.jsx`, `TextboxWidget.jsx`, `VisualizationWidget.jsx`, `VisualizationEmbed.jsx`)
diff --git a/client/app/components/dashboards/TextboxDialog.jsx b/client/app/components/dashboards/TextboxDialog.jsx
index 4ca904ca37..017aff6d2f 100644
--- a/client/app/components/dashboards/TextboxDialog.jsx
+++ b/client/app/components/dashboards/TextboxDialog.jsx
@@ -1,5 +1,5 @@
import { toString } from "lodash";
-import { markdown } from "markdown";
+import { marked } from "marked";
import React, { useState, useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import { useDebouncedCallback } from "use-debounce";
@@ -20,11 +20,11 @@ function TextboxDialog({ dialog, isNew, ...props }) {
useEffect(() => {
setText(props.text);
- setPreview(markdown.toHTML(props.text));
+ setPreview(marked.parse(props.text || ""));
}, [props.text]);
const [updatePreview] = useDebouncedCallback(() => {
- setPreview(markdown.toHTML(text));
+ setPreview(marked.parse(text || ""));
}, 200);
const handleInputChange = useCallback(
diff --git a/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx b/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx
index 79d9f68af8..fe0ecae455 100644
--- a/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx
+++ b/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
-import { markdown } from "markdown";
+import { marked } from "marked";
import Menu from "antd/lib/menu";
import HtmlContent from "@redash/viz/lib/components/HtmlContent";
import TextboxDialog from "@/components/dashboards/TextboxDialog";
@@ -32,7 +32,7 @@ function TextboxWidget(props) {
return (
- {markdown.toHTML(text || "")}
+ {marked.parse(text || "")}
);
}
diff --git a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx
index 9a021cc8bd..50c460560d 100644
--- a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx
+++ b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx
@@ -1,7 +1,7 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { compact, isEmpty, invoke, map } from "lodash";
-import { markdown } from "markdown";
+import { marked } from "marked";
import cx from "classnames";
import Menu from "antd/lib/menu";
import HtmlContent from "@redash/viz/lib/components/HtmlContent";
@@ -107,7 +107,7 @@ function VisualizationWidgetHeader({
{!isEmpty(widget.getQuery().description) && (
- {markdown.toHTML(widget.getQuery().description || "")}
+ {marked.parse(widget.getQuery().description || "")}
)}
diff --git a/client/app/pages/queries/VisualizationEmbed.jsx b/client/app/pages/queries/VisualizationEmbed.jsx
index a4bcaf3177..a2ff3542e1 100644
--- a/client/app/pages/queries/VisualizationEmbed.jsx
+++ b/client/app/pages/queries/VisualizationEmbed.jsx
@@ -2,7 +2,7 @@ import { find, has } from "lodash";
import React, { useState, useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import moment from "moment";
-import { markdown } from "markdown";
+import { marked } from "marked";
import Button from "antd/lib/button";
import Dropdown from "antd/lib/dropdown";
@@ -40,7 +40,7 @@ function VisualizationEmbedHeader({ queryName, queryDescription, visualization }
{queryName}
{queryDescription && (
- {markdown.toHTML(queryDescription || "")}
+ {marked.parse(queryDescription || "")}
)}
diff --git a/package.json b/package.json
index fad00bef53..18964b153c 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,7 @@
"font-awesome": "^4.7.0",
"history": "^4.10.1",
"hoist-non-react-statics": "^3.3.0",
- "markdown": "0.5.0",
+ "marked": "^4.3.0",
"material-design-iconic-font": "^2.2.0",
"mousetrap": "^1.6.1",
"mustache": "^2.3.0",
@@ -105,6 +105,7 @@
"babel-plugin-istanbul": "^6.1.1",
"babel-plugin-transform-builtin-extend": "^1.1.2",
"copy-webpack-plugin": "^13.0.1",
+ "core-js": "^2.6.12",
"css-loader": "^7.1.4",
"cypress": "^11.2.0",
"dayjs": "^1.11.9",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ba5adae8fb..cc5b8934e1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -59,9 +59,9 @@ importers:
hoist-non-react-statics:
specifier: ^3.3.0
version: 3.3.2
- markdown:
- specifier: 0.5.0
- version: 0.5.0
+ marked:
+ specifier: ^4.3.0
+ version: 4.3.0
material-design-iconic-font:
specifier: ^2.2.0
version: 2.2.0
@@ -198,6 +198,9 @@ importers:
copy-webpack-plugin:
specifier: ^13.0.1
version: 13.0.1(webpack@5.105.3)
+ core-js:
+ specifier: ^2.6.12
+ version: 2.6.12
css-loader:
specifier: ^7.1.4
version: 7.1.4(webpack@5.105.3)
@@ -2579,9 +2582,6 @@ packages:
'@xtuc/long@4.2.2':
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
- abbrev@1.1.1:
- resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
-
abs-svg-path@0.1.1:
resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==}
@@ -6004,8 +6004,9 @@ packages:
resolution: {integrity: sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==}
engines: {node: '>=16.14.0', npm: '>=8.1.0'}
- markdown@0.5.0:
- resolution: {integrity: sha512-ctGPIcuqsYoJ493sCtFK7H4UEgMWAUdXeBhPbdsg1W0LsV9yJELAHRsMmWfTgao6nH0/x5gf9FmsbxiXnrgaIQ==}
+ marked@4.3.0:
+ resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==}
+ engines: {node: '>= 12'}
hasBin: true
material-design-iconic-font@2.2.0:
@@ -6286,10 +6287,6 @@ packages:
node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
- nopt@2.1.2:
- resolution: {integrity: sha512-x8vXm7BZ2jE1Txrxh/hO74HTuYZQEbo8edoRcANgdZ4+PCV+pbjd/xdummkmjjC7LU5EjPzlu8zEq/oxWylnKA==}
- hasBin: true
-
normalize-package-data@2.5.0:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
@@ -11355,8 +11352,6 @@ snapshots:
'@xtuc/long@4.2.2': {}
- abbrev@1.1.1: {}
-
abs-svg-path@0.1.1: {}
accepts@1.3.8:
@@ -15743,9 +15738,7 @@ snapshots:
tinyqueue: 3.0.0
vt-pbf: 3.1.3
- markdown@0.5.0:
- dependencies:
- nopt: 2.1.2
+ marked@4.3.0: {}
material-design-iconic-font@2.2.0: {}
@@ -16013,10 +16006,6 @@ snapshots:
node-releases@2.0.27: {}
- nopt@2.1.2:
- dependencies:
- abbrev: 1.1.1
-
normalize-package-data@2.5.0:
dependencies:
hosted-git-info: 2.8.9