diff --git a/.github/workflows/pgdg-cve-scraper.yml b/.github/workflows/pgdg-cve-scraper.yml new file mode 100644 index 0000000..97b64bb --- /dev/null +++ b/.github/workflows/pgdg-cve-scraper.yml @@ -0,0 +1,110 @@ +name: pgdg-cve-scraper + +# PGDG ships security updates on Tuesdays (every ~4 weeks). This runs +# weekly Monday evenings UTC and opens a PR with newly-disclosed CVEs +# so a human can review + merge. Manual trigger via workflow_dispatch. + +on: + schedule: + # Mondays 22:00 UTC. Picked so we land after PGDG's Tuesday-batch of + # fixes (i.e., the *next* Monday after a PGDG release). + - cron: '0 22 * * 1' + workflow_dispatch: + # Note: deliberately no `pull_request:` trigger. The default GITHUB_TOKEN + # on PR events is read-only, so this workflow's "open a new PR" step + # would fail with "GitHub Actions is not permitted to create or approve + # pull requests." If you want a PR-validation pass, add a separate + # workflow that just runs the scraper in --check mode. + +concurrency: + group: pgdg-cve-scraper + cancel-in-progress: false + +permissions: {} + +jobs: + scrape: + permissions: + contents: write + pull-requests: write + name: Scrape PGDG security index + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + with: + python-version: "3.11" + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Sync deps + run: uv sync --quiet + + - name: Run scraper (CI mode) + # --check exit codes: + # 0 = no proposed additions + # 1 = proposed additions, written to stdout (this is what we want + # to open a PR for) + # 2+ = hard failure (parse error, network error, missing + # sentinel, etc.). Propagate as a workflow failure WITHOUT + # writing the (likely empty or partial) stdout, so the + # resulting commit doesn't carry a misleading file. + id: scrape + run: | + set +e + PROPOSED=$(uv run python tools/scrape_pgdg.py --check 2>/dev/null) + rc=$? + set -e + echo "scraper exit code: $rc" + if [ $rc -eq 1 ]; then + echo "needs_pr=true" >> "$GITHUB_OUTPUT" + echo "$PROPOSED" > proposed-cves.json + elif [ $rc -eq 0 ]; then + echo "needs_pr=false" >> "$GITHUB_OUTPUT" + else + echo "scraper: hard failure (exit code $rc); not opening a PR" >&2 + exit "$rc" + fi + + - name: Open PR with proposed additions + if: steps.scrape.outputs.needs_pr == 'true' + uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0 + with: + # Explicit base is required when the push event delivers a merge + # commit (detached HEAD), e.g. after merging main into the source + # branch on a feature PR. Default would otherwise fail with + # "the 'base' input must be supplied." + base: ${{ github.event.repository.default_branch }} + branch: chore/pgdg-cve-scrape + title: "chore: add newly disclosed PostgreSQL CVEs" + body: | + Automated PGDG security-index scrape. + + The scraper (`tools/scrape_pgdg.py`) ran against + https://www.postgresql.org/support/security/?cve=title and + found CVE entries newer than what's in `data/cves.json`. The + proposed additions are in `proposed-cves.json` below. + + **Action items for a human:** + - [ ] Eyeball each entry: confirm it actually affects + PostgreSQL 15-18. + - [ ] Sanity-check summaries (one-line, technical). + - [ ] Confirm CVSS thresholds (tool defaults to >= 7.0). + - [ ] Merge the JSON into `data/cves.json`: + ```bash + uv run python tools/scrape_pgdg.py --write --yes + uv run python tools/generate_cve_sql.py + git add data/cves.json pgFirstAid.sql view_pgFirstAid.sql view_pgFirstAid_managed.sql + git commit -m "chore: refresh CVE catalog" + ``` + - [ ] Delete `proposed-cves.json` before merge. + + scraper exit code: ${{ steps.scrape.outputs.needs_pr }} (non-zero = additions pending) + add-paths: proposed-cves.json + commit-message: "chore: scrape PGDG for new CVEs" + delete-branch: true + labels: automated,security diff --git a/.github/workflows/release-notes-scout.yml b/.github/workflows/release-notes-scout.yml new file mode 100644 index 0000000..71d5c11 --- /dev/null +++ b/.github/workflows/release-notes-scout.yml @@ -0,0 +1,117 @@ +name: release-notes-scout + +# Weekly Monday evening UTC scout of PostgreSQL release notes. +# Proposes high/medium severity bug fixes that match data/known_bugs.json +# entries missing from the curated list. A PR is opened for human review; +# never auto-merges. + +on: + schedule: + - cron: '0 22 * * 1' # Mondays 22:00 UTC, after PGDG's Tuesday batch. + workflow_dispatch: + # Note: deliberately no `pull_request:` trigger. The default GITHUB_TOKEN + # on PR events is read-only, so this workflow's "open a new PR" step + # would fail with "GitHub Actions is not permitted to create or approve + # pull requests." If you want a PR-validation pass, add a separate + # workflow that just runs the scout in --check mode. + +concurrency: + group: release-notes-scout + cancel-in-progress: false + +permissions: {} + +jobs: + scout: + permissions: + contents: write + pull-requests: write + name: Scout release-notes for high-severity bug fixes + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + with: + python-version: "3.11" + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Sync deps + run: uv sync --quiet + + - name: Run scout (CI mode) + # --check exit codes: + # 0 = no proposed additions + # 1 = proposed additions, written to stdout (this is what we want + # to open a PR for) + # 2+ = hard failure (parse error, network error, missing + # sentinel, etc.). Propagate as a workflow failure WITHOUT + # writing the (likely empty or partial) stdout, so the + # resulting commit doesn't carry a misleading file. + id: scout + run: | + set +e + PROPOSED=$(uv run python tools/scrape_release_notes.py --check 2>/dev/null) + rc=$? + set -e + echo "scout exit code: $rc" + if [ $rc -eq 1 ]; then + echo "needs_pr=true" >> "$GITHUB_OUTPUT" + echo "$PROPOSED" > proposed-bugs.json + elif [ $rc -eq 0 ]; then + echo "needs_pr=false" >> "$GITHUB_OUTPUT" + else + echo "scout: hard failure (exit code $rc); not opening a PR" >&2 + exit "$rc" + fi + + - name: Open PR with proposed bug entries + if: steps.scout.outputs.needs_pr == 'true' + uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0 + with: + # Explicit base is required when the push event delivers a merge + # commit (detached HEAD), e.g. after merging main into the source + # branch on a feature PR. Default would otherwise fail with + # "the 'base' input must be supplied." + base: ${{ github.event.repository.default_branch }} + branch: chore/release-notes-scout + title: "chore: add notable PostgreSQL bug fixes" + body: | + Automated PostgreSQL release-notes scout. + + `tools/scrape_release_notes.py` fetched the latest ~2 minor + release notes per major in {15,16,17,18}, filtered the + `
| cells.
+_TABLE_BODY_RE = re.compile(
+ r' | ||||
| ]*>(.*?) | ", re.DOTALL) + +_CVE_LINK_RE = re.compile(r'href="(/support/security/(CVE-\d{4}-\d+)/)"') +_CVSS_RE = re.compile( + r'href="https://nvd\.nist\.gov/vuln-metrics[^"]*"[^>]*>([0-9.]+)' +) +_VERSION_RE = re.compile(r"^(\d+)\.(\d+)$") + + +def parse_pgdg_table(html_text: str, *, majors: set[str] | None = None) -> list[dict]: + """Parse the PGDG security index and return one dict per CVE entry. + + If `majors` is provided, only entries whose `fixed_in` map contains at + least one of those majors are kept. Without it, the function preserves + every entry whose `fixed_in` has any major. The caller is expected to + apply any severity / major-scope filter separately. + + Returned dict shape: + { + "cve_id": "CVE-YYYY-NNNN", + "cvss": float, + "summary": str, # one-line description from the index + "doc_link": "https://www.postgresql.org/support/security/CVE-YYYY-NNNN/", + "fixed_in": {"15": 19, "16": 15, "17": 11, "18": 5}, # subset for our majors + "affected": ["15","16","17","18"], # raw major list (debug) + "fixed_raw": ["15.19","16.15",...], # raw version list (debug) + } + """ + m = _TABLE_BODY_RE.search(html_text) + if not m: + raise RuntimeError("PGDG index table not found; markup may have changed") + body = m.group(1) + + entries: list[dict] = [] + for row_match in _ROW_RE.finditer(body): + cells = _TD_RE.findall(row_match.group(1)) + if len(cells) != 5: + continue # skip thead/sub-header rows / malformed rows + + # ----- Cell 0: Reference ----- + cve_m = _CVE_LINK_RE.search(cells[0]) + if not cve_m: + continue + cve_id = cve_m.group(2) + doc_link = PGDG_BASE + cve_m.group(1) + + # ----- Cell 1: Affected majors ----- + affected_str = _strip_html(cells[1]) + affected_majors = [m.strip() for m in affected_str.split(",") if m.strip()] + if not affected_majors: + continue + + # ----- Cell 2: Fixed versions (positional to Affected) ----- + fixed_str = _strip_html(cells[2]) + fixed_versions = [m.strip() for m in fixed_str.split(",") if m.strip()] + if len(fixed_versions) != len(affected_majors): + # Out-of-sync table; bail on this row rather than mis-map. + continue + + fixed_in: dict[str, int] = {} + for major, version in zip(affected_majors, fixed_versions): + if majors is not None and major not in majors: + continue # skip majors outside the caller's scope + vm = _VERSION_RE.match(version) + if not vm or int(vm.group(1)) != int(major): + continue # malformed "X.Y" or major/version mismatch in upstream table + fixed_in[major] = int(vm.group(2)) + + if not fixed_in: + continue # no impact on PG 15-18 + + # ----- Cell 3: Component + CVSS score ----- + cvss_m = _CVSS_RE.search(cells[3]) + if not cvss_m: + continue + try: + cvss = float(cvss_m.group(1)) + except ValueError: + continue + + # ----- Cell 4: Description ----- + summary = _strip_html(cells[4]) + summary = re.sub(r"\s*more details\s*$", "", summary, flags=re.IGNORECASE) + if not summary: + continue + + entries.append({ + "cve_id": cve_id, + "cvss": cvss, + "summary": summary, + "doc_link": doc_link, + "fixed_in": fixed_in, + # Internal fields for downstream tooling (the writer strips these). + "_affected": affected_majors, + "_fixed_raw": fixed_versions, + }) + return entries + + +# ---------- Filtering ---------- + +def filter_proposed( + entries: list[dict], + existing: list[dict], + *, + min_cvss: float, + majors: list[str], +) -> list[dict]: + """Return entries that should be ADDED to data/cves.json. + + Rule: must affect at least one of `majors`, must be at least `min_cvss`, + must not already be present by cve_id. + """ + majors_set = set(majors) + existing_ids = {row["cve_id"] for row in existing} + additions: list[dict] = [] + for entry in entries: + if entry["cve_id"] in existing_ids: + continue # already curated + if entry["cvss"] < min_cvss: + continue # below severity threshold + if not (set(entry["fixed_in"]) & majors_set): + continue # doesn't affect any of our majors + additions.append(entry) + # Highest CVSS first; deterministic tiebreaker on id. + additions.sort(key=lambda e: (-e["cvss"], e["cve_id"])) + return additions + + +def to_json_format(proposed: list[dict]) -> list[dict]: + """Strip internal fields; return only the keys the JSON schema uses.""" + return [ + { + "cve_id": p["cve_id"], + "cvss": p["cvss"], + "summary": p["summary"], + "doc_link": p["doc_link"], + "fixed_in": p["fixed_in"], + } + for p in proposed + ] + + +# ---------- File IO ---------- + +def load_existing() -> list[dict]: + if not CVES_JSON.exists(): + return [] + return json.loads(CVES_JSON.read_text()).get("cves", []) + + +def merge_into_data(proposed: list[dict]) -> None: + """Patch data/cves.json in place with the proposed additions.""" + doc = json.loads(CVES_JSON.read_text()) + doc.setdefault("cves", []) + by_id = {row["cve_id"]: row for row in doc["cves"]} + for row in to_json_format(proposed): + if row["cve_id"] in by_id: + continue + doc["cves"].append(row) + # Bump last_reviewed. + from datetime import date + doc["last_reviewed"] = date.today().isoformat() + # Re-sort: by CVSS desc, then cve_id, for stable diffs. + doc["cves"].sort(key=lambda r: (-float(r["cvss"]), r["cve_id"])) + CVES_JSON.write_text(json.dumps(doc, indent=2, ensure_ascii=False) + "\n") + + +# ---------- HTTP ---------- + +def _resolve_cafile() -> str | None: + """Find a usable CA bundle, or None to fall back to system default. + + Some Python builds (notably Python 3.14 on Nix) ship with ssl.get_default_verify_paths() + pointing at /etc/ssl/cert.pem that doesn't exist, which makes urllib + fail with CERTIFICATE_VERIFY_FAILED. We try a few well-known paths. + """ + import ssl + import os + candidates = [ + os.environ.get("SSL_CERT_FILE"), + os.environ.get("CURL_CA_BUNDLE"), + "/etc/ssl/certs/ca-certificates.crt", # Debian/Ubuntu/GitHub Actions + "/etc/ssl/certs/ca-bundle.crt", # RHEL/CentOS + "/etc/pki/tls/certs/ca-bundle.crt", # older RHEL + "/etc/ssl/cert.pem", # macOS / some Python distros + ] + for path in candidates: + if path and os.path.exists(path): + return path + # Fall back to whatever Python's ssl layer thinks is correct. + default = ssl.get_default_verify_paths() + return default.cafile or (default.capath if os.path.isdir(default.capath or "") else None) + + +def fetch_pgdg_index(url: str = PGDG_URL, user_agent: str = DEFAULT_USER_AGENT, timeout: float = 30.0) -> str: + import ssl + ctx = ssl.create_default_context() + cafile = _resolve_cafile() + if cafile and os.path.isfile(cafile): + ctx.load_verify_locations(cafile) + elif cafile and os.path.isdir(cafile): + ctx.load_verify_locations(capath=cafile) + req = urllib.request.Request( + url, + headers={"User-Agent": user_agent, "Accept": "text/html,application/xhtml+xml"}, + ) + with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp: + return resp.read().decode("utf-8", "replace") + + +# ---------- CLI ---------- + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0]) + mode = p.add_mutually_exclusive_group() + mode.add_argument("--propose", action="store_true", default=True, + help="print proposed additions as JSON (default)") + mode.add_argument("--check", action="store_true", + help="CI mode: exit non-zero when additions are pending") + mode.add_argument("--write", action="store_true", + help="patch data/cves.json with proposed additions (use with --yes)") + p.add_argument("--yes", action="store_true", + help="skip confirmation prompt for --write") + p.add_argument("--min-cvss", type=float, default=DEFAULT_MIN_CVSS, + help=f"include CVEs at or above this CVSS v3 score (default {DEFAULT_MIN_CVSS})") + p.add_argument("--majors", default=",".join(DEFAULT_MAJORS), + help=f"comma-separated PG major versions to consider (default {','.join(DEFAULT_MAJORS)})") + p.add_argument("--url", default=PGDG_URL, help="PGDG URL (override for testing)") + p.add_argument("--user-agent", default=DEFAULT_USER_AGENT, + help="User-Agent header for the HTTP request") + args = p.parse_args(argv) + + majors = [m.strip() for m in args.majors.split(",") if m.strip()] + + # ---- Fetch + parse ---- + try: + html = fetch_pgdg_index(url=args.url, user_agent=args.user_agent) + except Exception as exc: + print(f"scraper: HTTP fetch failed: {exc}", file=sys.stderr) + return 2 + + try: + entries = parse_pgdg_table(html, majors=set(majors)) + except RuntimeError as exc: + print(f"scraper: parse failed: {exc}", file=sys.stderr) + return 2 + + # ---- Filter + propose ---- + existing = load_existing() + proposed = filter_proposed(entries, existing, min_cvss=args.min_cvss, majors=majors) + cleaned = to_json_format(proposed) + + # ---- Mode dispatch ---- + if args.check: + if cleaned: + print(f"FAIL: {len(cleaned)} new CVE(s) pending for review " + f"(see data/cves.json and propose output)", file=sys.stderr) + print(json.dumps(cleaned, indent=2, ensure_ascii=False)) + return 1 + print("ok: no new CVEs pending") + return 0 + + if args.write: + if not cleaned: + print("scraper: --write invoked but no new CVEs to add", file=sys.stderr) + return 0 + if not args.yes: + print("scraper: --write requires --yes to confirm overwrite of data/cves.json", file=sys.stderr) + return 2 + merge_into_data(proposed) + print(f"scraper: wrote {len(cleaned)} entry/entries into {CVES_JSON.relative_to(REPO_ROOT)}") + return 0 + + # Default: print proposed as JSON + print(json.dumps(cleaned, indent=2, ensure_ascii=False)) + if cleaned: + print(f"\n# ({len(cleaned)} proposed addition(s); threshold CVSS >= {args.min_cvss}, majors {majors})", + file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/scrape_release_notes.py b/tools/scrape_release_notes.py new file mode 100644 index 0000000..e63efa1 --- /dev/null +++ b/tools/scrape_release_notes.py @@ -0,0 +1,482 @@ +#!/usr/bin/env python3 +"""Scrape PostgreSQL release notes and propose notable bug fixes. + +Source of truth for the live catalog: + https://www.postgresql.org/docs/release/ (release index, lists all minors) + https://www.postgresql.org/docs/release/X.Y/ (per-minor change list) + +Workflow: + Default --propose Fetch latest releases, extract bug-fix listitems, classify + severity, dedupe against data/known_bugs.json, print the + top-N proposals as JSON. No files are modified. + --check CI mode: exit non-zero when proposals are non-empty. + --write --yes Patch data/known_bugs.json with the proposals. + +Inclusion rule (project policy, matches the prior conversation): + - Limit to the last `revisions-back` releases per major in {15,16,17,18}. + - Each proposal needs a stable identifier. We use the upstream commit hash + when available (`https://postgr.es/c/
| Reference | +Affected | +Fixed | +Component & CVSS v3 Base Score | +Description | +
|---|---|---|---|---|
|
+ CVE-2026-19385 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +client + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL pg_dump heap buffer overflow executes arbitrary code more details |
+
|
+ CVE-2026-18408 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +client + 8.8 AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H + |
+ PostgreSQL psql \unrestrict lets superuser of pg_dump origin server execute arbitrary code in psql client more details |
+
|
+ CVE-2026-18024 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +core server + 4.3 AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N + |
+ PostgreSQL ascii() function reads past end of buffer more details |
+
|
+ CVE-2026-16241 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +client + 3.8 AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:L + |
+ PostgreSQL ECPG integer underflow can crash the client more details |
+
|
+ CVE-2026-16239 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +core server + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL type confusion in cursor CLOSE + DECLARE executes arbitrary code more details |
+
|
+ CVE-2026-16238 + Announcement + |
+ 18 | +18.5 | +core server + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL type confusion in pg_restore_attribute_stats() executes arbitrary code more details |
+
|
+ CVE-2026-15742 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +contrib module + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL fuzzystrmatch writes effectively-arbitrary addresses, via integer wraparound more details |
+
|
+ CVE-2026-15741 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +core server + 8.8 AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H + |
+ PostgreSQL expression deparse allows SQL injection via EXTRACT argument more details |
+
|
+ CVE-2026-14681 + Announcement + |
+ 18, 17 | +18.5, 17.11 | +core server + 4.2 AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N + |
+ PostgreSQL improper enforcement of GSSAPI encryption when coupled with SSL more details |
+
|
+ CVE-2026-14680 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +core server + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL type confusion via "internal" arguments more details |
+
|
+ CVE-2026-14679 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +core server + 8.2 AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H + |
+ PostgreSQL stack buffer overflow in argument match writes 0x0 and 0x1 to server memory more details |
+
|
+ CVE-2026-14678 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +contrib module + 4.3 AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N + |
+ PostgreSQL pg_trgm picksplit reads past end of buffer more details |
+
|
+ CVE-2026-14677 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +core server + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL 32-bit pltcl and plperl undersize allocations, via integer wraparound more details |
+
|
+ CVE-2026-14676 + Announcement + |
+ 18 | +18.5 | +contrib module + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL pg_stat_statements heap buffer overflow executes arbitrary code more details |
+
|
+ CVE-2026-14673 + Announcement + |
+ 18, 16, 15, 14 | +18.5, 16.15, 15.19, 14.24 | +contrib module + 3.8 AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N + |
+ PostgreSQL amcheck does not clear untrusted search path more details |
+
|
+ CVE-2026-14672 + Announcement + |
+ 18, 17, 16 | +18.5, 17.11, 16.15 | +core server + 5.3 AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N + |
+ PostgreSQL observable response discrepancy with non-default scram_iterations provides user existence oracle more details |
+
|
+ CVE-2026-14671 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +contrib module + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL refint plan cache type confusion executes arbitrary code more details |
+
|
+ CVE-2026-14670 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +core server + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL plperl tied object heap buffer overflow executes arbitrary code more details |
+
|
+ CVE-2026-14669 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +core server + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL to_char heap buffer overflow executes arbitrary code more details |
+
|
+ CVE-2026-14668 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +core server + 8.1 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:H + |
+ PostgreSQL ctid type confusion in selectivity estimator discloses derivative of arbitrary read more details |
+
|
+ CVE-2026-14666 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +core server + 4.2 AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N + |
+ PostgreSQL row security caching disregards role modifications more details |
+
|
+ CVE-2026-14664 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +core server + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL regexp heap buffer overflow executes arbitrary code more details |
+
|
+ CVE-2026-14663 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +contrib module + 6.5 AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N + |
+ PostgreSQL pgcrypto, for OpenSSL-disabled ciphers, silently encrypts to and decrypts from cleartext more details |
+
|
+ CVE-2026-14662 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +core server + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL tsvector and tsquery undersize allocations, via integer wraparound more details |
+
|
+ CVE-2026-6638 + Announcement + |
+ 18, 17, 16 | +18.4, 17.10, 16.14 | +core server + 3.7 AV:N/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:N + |
+ PostgreSQL REFRESH PUBLICATION allows SQL injection via table name more details |
+
|
+ CVE-2026-6637 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.4, 17.10, 16.14, 15.18, 14.23 | +contrib module + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL refint allows stack buffer overflow and SQL injection more details |
+
|
+ CVE-2026-6575 + Announcement + |
+ 18 | +18.4 | +core server + 4.3 AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N + |
+ PostgreSQL pg_restore_attribute_stats accepts values that cause query planning to read past end of stats array more details |
+
|
+ CVE-2026-6479 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.4, 17.10, 16.14, 15.18, 14.23 | +core server + 7.5 AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H + |
+ PostgreSQL SSL/GSS init causes denial of service, via uncontrolled recursion more details |
+
|
+ CVE-2026-6478 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.4, 17.10, 16.14, 15.18, 14.23 | +core server + 6.5 AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N + |
+ PostgreSQL discloses MD5-hashed passwords via covert timing channel more details |
+
|
+ CVE-2026-6477 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.4, 17.10, 16.14, 15.18, 14.23 | +client + 8.8 AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H + |
+ PostgreSQL libpq lo_* functions let server superuser overwrite client stack memory more details |
+
|
+ CVE-2026-6476 + Announcement + |
+ 18, 17 | +18.4, 17.10 | +client + 7.2 AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL pg_createsubscriber allows SQL injection via subscription name more details |
+
|
+ CVE-2026-6475 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.4, 17.10, 16.14, 15.18, 14.23 | +client + 8.8 AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H + |
+ PostgreSQL pg_basebackup and pg_rewind can overwrite unrelated files of origin superuser choice more details |
+
|
+ CVE-2026-6474 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.4, 17.10, 16.14, 15.18, 14.23 | +core server + 4.3 AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N + |
+ PostgreSQL timeofday() can disclose portions of server memory more details |
+
|
+ CVE-2026-6473 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.4, 17.10, 16.14, 15.18, 14.23 | +core server + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL server undersizes allocations, via integer wraparound more details |
+
|
+ CVE-2026-6472 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.4, 17.10, 16.14, 15.18, 14.23 | +core server + 5.4 AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N + |
+ PostgreSQL CREATE TYPE does not check multirange schema CREATE privilege more details |
+
|
+ CVE-2026-6471 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +core server + 7.2 AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL logical decoding can dlopen arbitrary file more details |
+
|
+ CVE-2026-6470 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +core server + 4.3 AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L + |
+ PostgreSQL fails to check type USAGE privilege more details |
+
|
+ CVE-2026-6469 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +core server + 3.8 AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:L + |
+ PostgreSQL ALTER TABLE ALTER TYPE resets extended statistics ownership more details |
+
|
+ CVE-2026-6464 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +client + 8.1 AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL psql COPY FROM STDIN early failure processes data lines as psql commands more details |
+
|
+ CVE-2026-2007 + Announcement + |
+ 18 | +18.2 | +contrib module + 8.2 AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H + |
+ PostgreSQL pg_trgm heap buffer overflow writes pattern onto server memory more details |
+
|
+ CVE-2026-2006 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.2, 17.8, 16.12, 15.16, 14.21 | +core server + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL missing validation of multibyte character length executes arbitrary code more details |
+
|
+ CVE-2026-2005 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.2, 17.8, 16.12, 15.16, 14.21 | +contrib module + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL pgcrypto heap buffer overflow executes arbitrary code more details |
+
|
+ CVE-2026-2004 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.2, 17.8, 16.12, 15.16, 14.21 | +contrib module + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL intarray missing validation of type of input to selectivity estimator executes arbitrary code more details |
+
|
+ CVE-2026-2003 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.2, 17.8, 16.12, 15.16, 14.21 | +core server + 4.3 AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N + |
+ PostgreSQL oidvector discloses a few bytes of memory more details |
+
|
+ CVE-2025-12818 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.1, 17.7, 16.11, 15.15, 14.20 | +core server + 5.9 AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H + |
+ PostgreSQL libpq undersizes allocations, via integer wraparound more details |
+
|
+ CVE-2025-12817 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.1, 17.7, 16.11, 15.15, 14.20 | +core server + 3.1 AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L + |
+ PostgreSQL CREATE STATISTICS does not check for schema CREATE privilege more details |
+
|
+ CVE-2025-8715 + Announcement + |
+ 17, 16, 15, 14 | +17.6, 16.10, 15.14, 14.19 | +core server + 8.8 AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H + |
+ PostgreSQL pg_dump newline in object name executes arbitrary code in psql client and in restore target server more details |
+
|
+ CVE-2025-8714 + Announcement + |
+ 17, 16, 15, 14 | +17.6, 16.10, 15.14, 14.19 | +core server + 8.8 AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H + |
+ PostgreSQL pg_dump lets superuser of origin server execute arbitrary code in psql client more details |
+
|
+ CVE-2025-8713 + Announcement + |
+ 17, 16, 15, 14 | +17.6, 16.10, 15.14, 14.19 | +core server + 3.1 AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N + |
+ PostgreSQL optimizer statistics can expose sampled data within a view, partition, or child table more details |
+
|
+ CVE-2025-4207 + Announcement + |
+ 17, 16, 15, 14 | +17.5, 16.9, 15.13, 14.18 | +core server + 5.9 AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H + |
+ PostgreSQL GB18030 encoding validation can read one byte past end of allocation for text that fails validation more details |
+
|
+ CVE-2025-1094 + Announcement + |
+ 17, 16, 15, 14 | +17.3, 16.7, 15.11, 14.16 | +core server + 8.1 AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL quoting APIs miss neutralizing quoting syntax in text that fails encoding validation more details |
+
|
+ CVE-2024-10979 + Announcement + |
+ 17, 16, 15, 14 | +17.1, 16.5, 15.9, 14.14 | +core server + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL PL/Perl environment variable changes execute arbitrary code more details |
+
|
+ CVE-2024-10978 + Announcement + |
+ 17, 16, 15, 14 | +17.1, 16.5, 15.9, 14.14 | +core server + 4.2 AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N + |
+ PostgreSQL SET ROLE, SET SESSION AUTHORIZATION reset to wrong user ID more details |
+
|
+ CVE-2024-10977 + Announcement + |
+ 17, 16, 15, 14 | +17.1, 16.5, 15.9, 14.14 | +client + 3.1 AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:L/A:N + |
+ PostgreSQL libpq retains an error message from man-in-the-middle more details |
+
|
+ CVE-2024-10976 + Announcement + |
+ 17, 16, 15, 14 | +17.1, 16.5, 15.9, 14.14 | +core server + 4.2 AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N + |
+ PostgreSQL row security below e.g. subqueries disregards user ID changes more details |
+
|
+ CVE-2024-7348 + Announcement + |
+ 16, 15, 14 | +16.4, 15.8, 14.13 | +core server + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL relation replacement during pg_dump executes arbitrary SQL more details |
+
|
+ CVE-2024-4317 + Announcement + |
+ 16, 15, 14 | +16.3, 15.7, 14.12 | +core server + 3.1 AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N + |
+ Restrict visibility of "pg_stats_ext" and "pg_stats_ext_exprs" entries to the table owner more details |
+
|
+ CVE-2024-0985 + Announcement + |
+ 16, 15, 14 | +16.2, 15.6, 14.11 | +core server + 8.0 AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H + |
+ PostgreSQL non-owner REFRESH MATERIALIZED VIEW CONCURRENTLY executes arbitrary SQL more details |
+
|
+ CVE-2023-39418 + Announcement + |
+ 15 | +15.4 | +core server + 3.1 AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N + |
+ MERGE fails to enforce UPDATE or SELECT row security policies more details |
+
|
+ CVE-2023-39417 + Announcement + |
+ 15, 14 | +15.4, 14.9 | +core server + 7.5 AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ Extension script @substitutions@ within quoting allow SQL injection more details |
+
|
+ CVE-2023-5870 + Announcement + |
+ 16, 15, 14 | +16.1, 15.5, 14.10 | +core server + 2.2 AV:N/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:L + |
+ Role "pg_signal_backend" can signal certain superuser processes more details |
+
|
+ CVE-2023-5869 + Announcement + |
+ 16, 15, 14 | +16.1, 15.5, 14.10 | +core server + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ Buffer overrun from integer overflow in array modification more details |
+
|
+ CVE-2023-5868 + Announcement + |
+ 16, 15, 14 | +16.1, 15.5, 14.10 | +core server + 4.3 AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N + |
+ Memory disclosure in aggregate function calls more details |
+
|
+ CVE-2023-2455 + Announcement + |
+ 15, 14 | +15.3, 14.8 | +core server + 4.2 AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N + |
+ Row security policies disregard user ID changes after inlining more details |
+
|
+ CVE-2023-2454 + Announcement + |
+ 15, 14 | +15.3, 14.8 | +core server + 7.2 AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H + |
+ CREATE SCHEMA ... schema_element defeats protective search_path changes more details |
+
|
+ CVE-2022-41862 + Announcement + |
+ 15, 14 | +15.2, 14.7 | +client + 3.7 AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N + |
+ Client memory disclosure when connecting, with Kerberos, to modified server more details |
+
|
+ CVE-2022-2625 + Announcement + |
+ 14 | +14.5 | +core server + 7.1 AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H + |
+ Extension scripts replace objects not belonging to the extension more details |
+
|
+ CVE-2022-1552 + Announcement + |
+ 14 | +14.3 | +core server + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ Autovacuum, REINDEX, and others omit "security restricted operation" sandbox more details |
+
|
+ CVE-2021-23222 + Announcement + |
+ 14 | +14.1 | +client + 3.7 AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N + |
+ libpq processes unencrypted bytes from man-in-the-middle more details |
+
|
+ CVE-2021-23214 + Announcement + |
+ 14 | +14.1 | +core server + 8.1 AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H + |
+ Server processes unencrypted bytes from man-in-the-middle more details |
+
| Reference | Affected | Fixed | Component & CVSS v3 Base Score | Description |
|---|---|---|---|---|
| CVE-2026-19385 + Announcement + |
+ 18, 17, 16, 15, 14 | +18.5, 17.11, 16.15, 15.19, 14.24 | +client + 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + |
+ PostgreSQL pg_dump heap buffer overflow executes arbitrary code more details |
+
| CVE-2024-0985 + Announcement + |
+ 16, 15, 14 | +16.2, 15.6, 14.11 | +core server + 8.0 AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H + |
+ Non-owner REFRESH MATERIALIZED VIEW CONCURRENTLY executes arbitrary SQL more details |
+
Below is the complete archive of release notes for every version of PostgreSQL.
+ + +Fix memory-safety bugs in processing of incorrect ispell/hunspell dictionary files (Heikki Linnakangas) §
+Discovered while fuzzing.
+Fix crash with namespace nodes in contrib/xml2's xpath_nodeset() function (Andrey Chernyy, Michael Paquier) §
+In contrib/ltree, fix integer overflow in comparisons (Ayush Tiwari) §
+When a partition's detached partition is DETACH FINALIZE, fix a logical replication regression (Tomas Vondra) §
+Fix several typos in the documentation
+Fix unexpected subxact cache search-path for HOT-pruned tuples (no commit URL, no name)
+PostgreSQL "pg_dump" leaks files & arbitrary code
" + ) + self.assertEqual(out, 'PostgreSQL "pg_dump" leaks files & arbitrary code') + + +class FetchIndexUrlTests(unittest.TestCase): + """fetch_pgdg_index passes its `url` argument to urllib.request.Request. + + Without this, the CLI's --url flag is decorative — the request always + hits the hard-coded PGDG_URL. These tests verify the wiring. + """ + + def test_request_uses_supplied_url(self) -> None: + import unittest.mock as mock + + captured: dict = {} + def fake_urlopen(req, timeout, context): + captured["url"] = req.full_url + captured["headers"] = dict(req.header_items()) + return mock.MagicMock( + __enter__=lambda s: s, + __exit__=lambda s, *a: False, + read=lambda: b"placeholder", + ) + + with mock.patch("urllib.request.urlopen", side_effect=fake_urlopen): + scraper.fetch_pgdg_index(url="https://example.com/security") + + self.assertEqual(captured["url"], "https://example.com/security") + + def test_default_url_unchanged(self) -> None: + import unittest.mock as mock + + captured: dict = {} + def fake_urlopen(req, timeout, context): + captured["url"] = req.full_url + return mock.MagicMock( + __enter__=lambda s: s, + __exit__=lambda s, *a: False, + read=lambda: b"x", + ) + + with mock.patch("urllib.request.urlopen", side_effect=fake_urlopen): + scraper.fetch_pgdg_index() + + self.assertEqual(captured["url"], scraper.PGDG_URL) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tools/tests/test_scrape_release_notes.py b/tools/tests/test_scrape_release_notes.py new file mode 100644 index 0000000..f318972 --- /dev/null +++ b/tools/tests/test_scrape_release_notes.py @@ -0,0 +1,254 @@ +"""Tests for tools/scrape_release_notes.py. + +Frozen HTML fixtures + targeted unit tests for the parser, severity +classifier, and proposal-filter pipeline. +""" + +from __future__ import annotations + +import json +import sys +import tempfile +import unittest +from pathlib import Path + +TOOLS_DIR = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(TOOLS_DIR)) + +import scrape_release_notes as scout # noqa: E402 + +FIXTURES_DIR = TOOLS_DIR / "tests" / "fixtures" + + +def _load(name: str) -> str: + return (FIXTURES_DIR / name).read_text() + + +# ---------- Page parser ---------- + +class ParseReleasePageTests(unittest.TestCase): + + @classmethod + def setUpClass(cls) -> None: + cls.html = _load("release_notes_synthetic.html") + + def test_parses_all_listitems(self) -> None: + entries = scout.parse_release_page( + self.html, major="17", minor="11", + doc_link="https://www.postgresql.org/docs/release/17.11/", + ) + self.assertEqual(len(entries), 6) + + def test_severity_classifier_flags_data_integrity(self) -> None: + entries = scout.parse_release_page( + self.html, major="17", minor="11", + doc_link="x/", + ) + # The memory-safety bugs entry + mem_safety = next(e for e in entries if "memory-safety" in e["summary"]) + self.assertEqual(mem_safety["_severity"], "high") + + def test_severity_classifier_flags_crash(self) -> None: + entries = scout.parse_release_page(self.html, major="17", minor="11", doc_link="x/") + crash = next(e for e in entries if "crash" in e["summary"]) + self.assertEqual(crash["_severity"], "high") + + def test_severity_classifier_flags_replication_medium(self) -> None: + entries = scout.parse_release_page(self.html, major="17", minor="11", doc_link="x/") + repl = next(e for e in entries if "logical replication" in e["summary"]) + self.assertEqual(repl["_severity"], "medium") + + def test_severity_classifier_low_for_documentation(self) -> None: + entries = scout.parse_release_page(self.html, major="17", minor="11", doc_link="x/") + doc = next(e for e in entries if "documentation" in e["summary"]) + self.assertEqual(doc["_severity"], "low") + + def test_strips_trailing_author_attribution(self) -> None: + entries = scout.parse_release_page(self.html, major="17", minor="11", doc_link="x/") + for entry in entries: + # "(Author Name)" should be trimmed from the summary. + self.assertNotRegex(entry["summary"], r"\([A-Z][\w .'-]*(?:,.*?)*\)\s*$", + f"author not stripped: {entry['summary']!r}") + + def test_strips_trailing_section_marker(self) -> None: + entries = scout.parse_release_page(self.html, major="17", minor="11", doc_link="x/") + for entry in entries: + # The section anchor at the end (from postgr.es/c/