From cf93e191a93b483c1237e9ac1fff9aa2ed6e8f02 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 10 Nov 2026 18:12:49 +0900 Subject: [PATCH 1/3] Use connection-aware escaping for sets Convert set and frozenset values through Connection.literal so quoting uses the connection character set and SQL mode. Deprecate Set2Str, remove its default mapping, and dispatch built-in literals using exact type checks. --- src/MySQLdb/connections.py | 19 +++++++++++++------ src/MySQLdb/converters.py | 4 +++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/MySQLdb/connections.py b/src/MySQLdb/connections.py index 98847a65..cf0350f5 100644 --- a/src/MySQLdb/connections.py +++ b/src/MySQLdb/connections.py @@ -200,9 +200,7 @@ class object, used to create cursors (keyword only) "executemany_fallback", self.executemany_fallback ) if executemany_fallback not in ("loop", "multi"): - raise ValueError( - "executemany_fallback must be either 'loop' or 'multi'" - ) + raise ValueError("executemany_fallback must be either 'loop' or 'multi'") client_flag = kwargs.get("client_flag", 0) client_flag |= CLIENT.MULTI_RESULTS @@ -311,6 +309,12 @@ def _bytes_literal(self, bs): def _tuple_literal(self, t): return b"(%s)" % (b",".join(map(self.literal, t))) + def _set_literal(self, s): + for c in s: + if not isinstance(c, str): + raise TypeError("set elements must be strings") + return self.string_literal(",".join(s)) + def literal(self, o): """If o is a single object, returns an SQL literal as a string. If o is a non-string sequence, the items of the sequence are @@ -319,12 +323,15 @@ def literal(self, o): Non-standard. For internal use; do not use this in your applications. """ - if isinstance(o, str): + t = type(o) + if t is str: s = self.string_literal(o.encode(self.encoding)) - elif isinstance(o, (bytes, bytearray)): + elif t in (bytes, bytearray): s = self._bytes_literal(o) - elif isinstance(o, (tuple, list)): + elif t in (tuple, list): s = self._tuple_literal(o) + elif t in (set, frozenset): + s = self._set_literal(o) else: s = self.escape(o, self.encoders) if isinstance(s, str): diff --git a/src/MySQLdb/converters.py b/src/MySQLdb/converters.py index 0db7b447..29328f44 100644 --- a/src/MySQLdb/converters.py +++ b/src/MySQLdb/converters.py @@ -62,6 +62,9 @@ def Bool2Str(s, d): def Set2Str(s, d): # Only support ascii string. Not tested. + import warnings + + warnings.warn("Set2Str is deprecated", DeprecationWarning, stacklevel=2) return string_literal(",".join(s)) @@ -112,7 +115,6 @@ def array2Str(o, d): Date: Thing2Literal, DateTimeType: DateTime2literal, DateTimeDeltaType: DateTimeDelta2literal, - set: Set2Str, Decimal: Decimal2Literal, FIELD_TYPE.TINY: int, FIELD_TYPE.SHORT: int, From ff20ca7f679d329be71a98731ad063ccdbe00629 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 10 Nov 2026 18:29:31 +0900 Subject: [PATCH 2/3] Publish Windows distributions to PyPI on release tags Publish wheels and sdist through Trusted Publishing after both Windows builds succeed, only when a new v* tag is pushed. Lock the new Actions dependencies and document the release procedure. --- .github/workflows/actions.lock | 19 ++++++ .github/workflows/windows.yaml | 27 +++++++++ notes/release.md | 105 +++++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 notes/release.md diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index bd46794f..e87836e3 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -12,8 +12,10 @@ workflows: '.github/workflows/windows.yaml': - 'actions/cache@v6.1.0' - 'actions/checkout@v7.0.1' + - 'actions/download-artifact@v8.0.2' - 'actions/setup-python@v7.0.0' - 'actions/upload-artifact@v7.0.2' + - 'pypa/gh-action-pypi-publish@v1.14.3' dependencies: 'actions/cache@v6.1.0': ref: 'v6.1.0' @@ -25,11 +27,21 @@ dependencies: commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' owner_id: 44036562 repo_id: 197814629 + 'actions/download-artifact@v8.0.2': + ref: 'v8.0.2' + commit: 'sha1-9000827ccba6bdab643e8b6fd33ac0654aef8333' + owner_id: 44036562 + repo_id: 192626254 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd': ref: 'v8.0.0' commit: 'sha1-ed597411d8f924073f98dfc5c65a23a2325f34cd' owner_id: 44036562 repo_id: 205262760 + 'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405': + ref: 'v6.2.0' + commit: 'sha1-a309ff8b426b58ec0e2a45f0f869d46889d02405' + owner_id: 44036562 + repo_id: 192625525 'actions/setup-python@v7.0.0': ref: 'v7.0.0' commit: 'sha1-5fda3b95a4ea91299a34e894583c3862153e4b97' @@ -47,3 +59,10 @@ dependencies: repo_id: 200299178 uses: - 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd' + 'pypa/gh-action-pypi-publish@v1.14.3': + ref: 'v1.14.3' + commit: 'sha1-7245906fb61fa33a13554f9d5fb856fe61758086' + owner_id: 647025 + repo_id: 178055147 + uses: + - 'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405' diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml index 3e972349..776e6060 100644 --- a/.github/workflows/windows.yaml +++ b/.github/workflows/windows.yaml @@ -3,9 +3,13 @@ name: Build windows wheels on: push: branches: ["main", "ci"] + tags: ["v*"] pull_request: workflow_dispatch: +permissions: + contents: read + jobs: build: strategy: @@ -107,3 +111,26 @@ jobs: with: name: win-wheels-${{ matrix.cibw_arch }} path: mysqlclient/dist/*.* + if-no-files-found: error + + publish: + name: Publish to PyPI + needs: build + if: >- + github.event_name == 'push' && github.event.created && + startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - name: Download distributions + uses: actions/download-artifact@v8.0.2 + with: + pattern: win-wheels-* + path: dist + merge-multiple: true + + - name: Publish distributions to PyPI + uses: pypa/gh-action-pypi-publish@v1.14.3 + with: + attestations: true diff --git a/notes/release.md b/notes/release.md new file mode 100644 index 00000000..4081f3fb --- /dev/null +++ b/notes/release.md @@ -0,0 +1,105 @@ +# Release procedure + +The examples below release **2.3.1** after **2.3.0**. Replace the version +numbers and release date as needed. Run commands from the repository root. + +Ensure the PyPI Trusted Publisher for `mysqlclient` is configured with +repository `PyMySQL/mysqlclient`, workflow `windows.yaml`, and no required +environment name. + +## 1. Update the release notes + +Review the changes since the previous release: + +```sh +git log v2.3.0..HEAD --oneline +git diff v2.3.0..HEAD --stat +``` + +Add a `What's new in 2.3.1` section at the top of +[HISTORY.rst](../HISTORY.rst), with `Release: YYYY-MM-DD` set to the release +date. Document changes, fixes, and deprecations that affect users. + +## 2. Update the version and create a release PR + +Update both version fields in +[src/MySQLdb/release.py](../src/MySQLdb/release.py): + +```python +__version__ = "2.3.1" +version_info = (2, 3, 1, "final", 0) +``` + +Format the source and run the checks: + +```sh +ruff format src/ +ruff check src/ +git diff --check +``` + +Review the diff, commit it as `release v2.3.1`, and open a PR targeting +`main` in `PyMySQL/mysqlclient`. + +## 3. Verify CI for the release commit + +Once the PR checks pass, merge it into `main`. Verify that all of the +following workflows pass for the merged commit: + +- `Lint` +- `Test`, including MySQL, MariaDB, and Django tests +- `Build windows wheels` + +To run the tests locally, install the package and the dependencies in +`requirements.txt`, prepare the database, and run the command used by the +[test workflow](../.github/workflows/tests.yaml). Adjust `TESTDB` to use +the configuration for your database: + +```sh +TESTDB=actions.cnf pytest --cov=MySQLdb tests +``` + +Verify that the [Windows workflow](../.github/workflows/windows.yaml) +produces both `win-wheels-AMD64` and `win-wheels-ARM64` artifacts. The AMD64 +artifact also contains the source distribution (sdist). + +Record the release commit SHA. + +## 4. Create and push the release tag + +Replace `RELEASE_SHA` with the commit SHA verified in step 3. Fetch the +commit, create a lightweight tag, and push it: + +```sh +git fetch upstream main --tags +git tag v2.3.1 RELEASE_SHA +git push upstream refs/tags/v2.3.1 +``` + +Pushing the new `v*` tag starts a build of the tagged commit. Once both +Windows builds succeed, the `Publish to PyPI` job combines their artifacts +and publishes the wheels and sdist using Trusted Publishing. +Publishing is restricted to push events that create a new `v*` tag. + +## 5. Verify publication and create the GitHub Release + +Check the tag-triggered `Build windows wheels` workflow and confirm that +both builds and the `Publish to PyPI` job succeed. Then create the GitHub +Release: + +```sh +gh release create v2.3.1 -R PyMySQL/mysqlclient \ + --verify-tag --title v2.3.1 \ + --generate-notes --notes-start-tag v2.3.0 +``` + +## 6. Verify the published release + +Check that PyPI lists the sdist and wheels for both architectures. Install +`mysqlclient==2.3.1` in a fresh environment and verify the import and version: + +```sh +python -c 'import MySQLdb; print(MySQLdb.version_info)' +``` + +The expected output is `(2, 3, 1, 'final', 0)`. From a57760990cb57e8c7bd3f79af41aaffb1ec81d01 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 10 Nov 2026 18:37:15 +0900 Subject: [PATCH 3/3] release v2.3.1 --- HISTORY.rst | 21 +++++++++++++++++++++ src/MySQLdb/release.py | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 91cebf04..1b07800a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,3 +1,24 @@ +====================== + What's new in 2.3.1 +====================== + +Release: 2026-11-10 + +* **Security fix:** Escape Python ``set`` query parameters using the connection's + character set and SQL mode. This fixes SQL injection when + ``NO_BACKSLASH_ESCAPES`` is enabled and an attacker controls a set member. + ``frozenset`` parameters now use the same escaping. + See `GHSA-m9c3-99x3-77g6 `_. + +* Deprecate ``MySQLdb.converters.Set2Str``. + +* ``Connection.literal()`` now applies built-in handling only to exact built-in + types. Subclasses use the connection's converter mapping. + +* Fix crashes when looking up connection attributes with a non-string name or + a string containing lone surrogates. (#810) + + ====================== What's new in 2.3.0 ====================== diff --git a/src/MySQLdb/release.py b/src/MySQLdb/release.py index 4c3fad89..8b54c83c 100644 --- a/src/MySQLdb/release.py +++ b/src/MySQLdb/release.py @@ -1,3 +1,3 @@ __author__ = "Inada Naoki " -__version__ = "2.3.0" -version_info = (2, 3, 0, "final", 0) +__version__ = "2.3.1" +version_info = (2, 3, 1, "final", 0)