From f842e16e326fc4c1f1312d728ea6011237facd4f Mon Sep 17 00:00:00 2001 From: Chad Lawlis Date: Wed, 17 Jun 2026 17:34:21 -0400 Subject: [PATCH 1/4] Add timeout to OAuth callback unblock request Bandit flagged the requests.get() call in auth.py used to unblock the local OAuth callback server's handle_request() loop as a call to requests without a timeout. Add an explicit timeout=10 and remove the now-unneeded missing-timeout pylint pragma. Co-Authored-By: Claude Opus 4.8 (1M context) --- felt/core/auth.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/felt/core/auth.py b/felt/core/auth.py index aa368f6..4b1f0af 100644 --- a/felt/core/auth.py +++ b/felt/core/auth.py @@ -181,9 +181,10 @@ def force_stop(): """ # we have to dummy a dummy request in order to abort the # blocking handle_request() loop - # pylint: disable=missing-timeout - requests.get("http://127.0.0.1:{}".format(REDIRECT_PORT)) - # pylint: enable=missing-timeout + requests.get( + "http://127.0.0.1:{}".format(REDIRECT_PORT), + timeout=10 + ) def close_server(self): """ From 67b72ca9d9fedda0f298a9097f5d8178a8660237 Mon Sep 17 00:00:00 2001 From: Chad Lawlis Date: Wed, 17 Jun 2026 17:34:28 -0400 Subject: [PATCH 2/4] Replace high-entropy multipart boundary with readable constant The secrets scanner flagged the hardcoded multipart/form-data boundary "QGISFormBoundary2XCkqVRLJ5XMxfw5" as a potential base64 high-entropy string. It was only a MIME boundary marker, not a secret. Replace it with a single readable, low-entropy constant (MULTIPART_BOUNDARY = "QGISFeltPluginFormBoundary") referenced everywhere the boundary is used, and update the matching test assertion. Co-Authored-By: Claude Opus 4.8 (1M context) --- felt/core/api_client.py | 15 +++++++++++---- felt/test/test_api_client.py | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/felt/core/api_client.py b/felt/core/api_client.py index 8375488..01523ec 100644 --- a/felt/core/api_client.py +++ b/felt/core/api_client.py @@ -72,6 +72,9 @@ class FeltApiClient: UPDATE_LAYER_ENDPOINT = '/maps/{}/layers' LAYER_GROUPS_ENDPOINT = '/maps/{}/layer_groups' + # boundary marker used when building multipart/form-data upload bodies + MULTIPART_BOUNDARY = 'QGISFeltPluginFormBoundary' + def __init__(self): # default headers to add to all requests self.headers = { @@ -338,15 +341,19 @@ def create_upload_file_request(self, b'Host', parameters.url[len('https://'):-1].encode() ) + boundary = self.MULTIPART_BOUNDARY network_request.setRawHeader( b"Content-Type", - b"multipart/form-data; boundary=QGISFormBoundary2XCkqVRLJ5XMxfw5") + "multipart/form-data; boundary={}".format(boundary).encode()) + + delimiter = "--{}\r\n".format(boundary).encode() + closing_delimiter = "--{}--\r\n".format(boundary).encode() # build the form content as bytes: PyQt6 does not permit appending # strings to QByteArray form_content = b'' for name, value in parameters.to_form_fields().items(): - form_content += b"--QGISFormBoundary2XCkqVRLJ5XMxfw5\r\n" + form_content += delimiter form_content += b"Content-Disposition: form-data; " form_content += f"name=\"{name}\"".encode() form_content += b"\r\n" @@ -354,7 +361,7 @@ def create_upload_file_request(self, form_content += str(value).encode() form_content += b"\r\n" - form_content += b"--QGISFormBoundary2XCkqVRLJ5XMxfw5\r\n" + form_content += delimiter form_content += b"Content-Disposition: " form_content += \ f"form-data; name=\"file\"; filename=\"{filename}\"\r\n".encode() @@ -364,7 +371,7 @@ def create_upload_file_request(self, form_content += content form_content += b"\r\n" - form_content += b"--QGISFormBoundary2XCkqVRLJ5XMxfw5--\r\n" + form_content += closing_delimiter form_data = QByteArray(form_content) content_length = form_data.length() diff --git a/felt/test/test_api_client.py b/felt/test/test_api_client.py index 398b9b1..34751a9 100644 --- a/felt/test/test_api_client.py +++ b/felt/test/test_api_client.py @@ -277,7 +277,7 @@ def test_create_upload_file_request(self): b'form-data; name="file"; filename="test.gpkg"', body) self.assertIn(b'GPKG\x00\x01binary', body) self.assertTrue( - body.endswith(b'--QGISFormBoundary2XCkqVRLJ5XMxfw5--\r\n')) + body.endswith(b'--QGISFeltPluginFormBoundary--\r\n')) self.assertEqual(request.rawHeader(b'Content-Length'), str(len(body)).encode()) From c4170a8c81e8480d0f2afb25c063edb488757a72 Mon Sep 17 00:00:00 2001 From: Chad Lawlis Date: Wed, 17 Jun 2026 17:50:38 -0400 Subject: [PATCH 3/4] Bump version to 3.2.1 for security fix release The 3.2.0 release was blocked from publication to the QGIS plugin repository by the security scan. Since a 3.2.0 GitHub release/tag was already cut against code without these fixes, bump to 3.2.1 for the fixed release rather than reusing the spent version number. Add the corresponding 3.2.1 entry to CHANGELOG.md (qgis-plugin-ci injects this into metadata.txt at release time). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 7 +++++++ felt/metadata.txt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69a21dc..3767f87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## [Unreleased] +## [3.2.1] - 2026-06-17 + +- Fix QGIS plugin repository security scan issues blocking release: + add an explicit timeout to the OAuth callback unblock request, and + replace the high-entropy multipart form boundary with a readable + constant + ## [3.2.0] - 2026-06-17 - Add support for QGIS 4.x (Qt6-based) releases, while remaining diff --git a/felt/metadata.txt b/felt/metadata.txt index c6be73e..51850c6 100644 --- a/felt/metadata.txt +++ b/felt/metadata.txt @@ -12,7 +12,7 @@ name=Add to Felt qgisMinimumVersion=3.22 qgisMaximumVersion=4.99 description=Create a collaborative Felt (felt.com) map from QGIS -version=3.2.0 +version=3.2.1 author=Felt email=support@felt.com From d1f4127717604362b5c258a7aaa23d0dc5453cdb Mon Sep 17 00:00:00 2001 From: Chad Lawlis Date: Wed, 17 Jun 2026 17:53:07 -0400 Subject: [PATCH 4/4] Clarify changelog handling in metadata and releasing docs Document that the metadata.txt changelog= field is auto-populated from CHANGELOG.md by qgis-plugin-ci at release time and must not be edited by hand. Correct RELEASING.md, which previously described CHANGELOG.md as cosmetic, to explain that its notes are injected into the published package and surfaced to users in the QGIS Plugin Manager. Add guidance for resubmitting under a new patch version when plugins.qgis.org blocks an upload after a tag has been cut. Co-Authored-By: Claude Opus 4.8 (1M context) --- RELEASING.md | 35 ++++++++++++++++++++++++----------- felt/metadata.txt | 4 +++- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 823675b..12b3fed 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -11,21 +11,27 @@ published, the [`release.yml`](.github/workflows/release.yml) workflow runs `felt/metadata.txt` `version=` field — overriding whatever value is committed there. -- Tags use **no `v` prefix** (e.g. `3.2.0`, not `v3.2.0`). The tag name becomes +- Tags use **no `v` prefix** (e.g. `3.2.1`, not `v3.2.1`). The tag name becomes the version string verbatim, so a `v` prefix would ship a version literally - named `v3.2.0`. -- The `version=` field in `felt/metadata.txt` and the entries in - `CHANGELOG.md` are cosmetic — they do not drive the release and are not - prominently surfaced to users. Keep them consistent for hygiene, but the tag - is what matters. + named `v3.2.1`. +- The `version=` field in `felt/metadata.txt` is **cosmetic** — `qgis-plugin-ci` + overrides it with the tag name at release time. Keep it consistent with the + tag for hygiene, but the tag is what determines the shipped version. +- `CHANGELOG.md` is **not** cosmetic. At release time `qgis-plugin-ci` reads it + and injects the matching version's notes into the packaged `metadata.txt` + `changelog=` field, which QGIS surfaces to users in the Plugin Manager. So the + `changelog=` field in `metadata.txt` is intentionally left empty and must not + be hand-edited — maintain release notes in `CHANGELOG.md` only. ## Steps -1. **(Optional) Update the changelog and metadata on a branch.** Move items out - of `[Unreleased]` in `CHANGELOG.md` into a new `## [] - ` - section, and bump `version=` in `felt/metadata.txt` to match. Open a PR and - merge to `main`. This is cosmetic hygiene, not required for the release to - succeed. +1. **Update the changelog and metadata on a branch.** Move items out of + `[Unreleased]` in `CHANGELOG.md` into a new `## [] - ` section, + and bump `version=` in `felt/metadata.txt` to match. Open a PR and merge to + `main`. The release will still build without this, but the `CHANGELOG.md` + entry is what populates the user-visible changelog in the published package + (see Versioning above), so do it before tagging. The `version=` bump itself is + cosmetic hygiene since the tag overrides it. 2. **Create a GitHub Release** with a new tag (e.g. `3.2.0`), targeting `main`. Publishing the release triggers [`release.yml`](.github/workflows/release.yml), @@ -47,3 +53,10 @@ there. builds an `-alpha` package via `qgis-plugin-ci package` and uploads it as a CI artifact (with a download link posted on the PR). This is for testing pre-release builds and is not part of the release path. +- **If plugins.qgis.org rejects the upload** (e.g. its automated security scan + blocks the package), do **not** reuse the same version number to resubmit. + The GitHub tag/release for that version is already cut against the old code + and should be treated as immutable. Fix the issues on a branch, bump to a new + patch version (e.g. `3.2.0` → `3.2.1`) with a matching `CHANGELOG.md` entry, + cut a new tag/release, and upload that. Leave the blocked release in place as + a record of the attempt. diff --git a/felt/metadata.txt b/felt/metadata.txt index 51850c6..0a75dec 100644 --- a/felt/metadata.txt +++ b/felt/metadata.txt @@ -24,7 +24,9 @@ repository=https://github.com/felt/qgis-plugin # Recommended items: -# Uncomment the following line and add your changelog: +# Leave this empty. The changelog is maintained in CHANGELOG.md and +# injected here automatically by qgis-plugin-ci at release time +# (see .github/workflows/release.yml). Do not edit this field by hand. changelog= # Tags are comma separated with spaces allowed