From 85ab738019fb953076be3b6790bde6fe4436c99b Mon Sep 17 00:00:00 2001 From: "gocardless-ci-robot[bot]" <123969075+gocardless-ci-robot[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:14:51 +0000 Subject: [PATCH] Changes generated by 104a0a3569d9c6de11b5288dac6200f82cc20217 This commit was automatically created from gocardless/client-library-templates@104a0a3569d9c6de11b5288dac6200f82cc20217 by the `push-files` action. Workflow run: https://github.com/gocardless/client-library-templates/actions/runs/37428166778 --- CHANGELOG.md | 36 ++++++++++++++++++++++++++++++++++++ gocardless_pro/__init__.py | 2 +- gocardless_pro/api_client.py | 4 ++-- setup.py | 2 +- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 433833ee..e12012aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,42 @@ # Changelog +## 3.18.0 (2026-10-06) + +### Features + +#### Reject request paths that would leave the configured base URL + +The low-level request path was joined onto the configured base URL with a resolving +join, which follows a path the way a browser follows a link. An absolute URL +(`https://host/x`) or a scheme-relative one (`//host/x`) replaced the configured +origin outright while the Authorization header was still attached, so an application +that passed untrusted input as a path could send its API token to a host of someone +else's choosing. + +A path that is not a relative reference is now rejected before the join. Paths +containing dot segments remain valid: they resolve against the base URL and cannot +leave its origin. + +#### Reject URL parameters that could change which endpoint is addressed + +A URL parameter is a single path segment — a resource identity — but the escaping +applied to one varied by language, and in Go, Node, PHP and .NET there was none at +all. A value carrying path syntax could move a request to an endpoint the caller +never asked for: `find("../mandates")` reached the mandates collection, and +`find("?limit=500")` injected a query parameter. + +Escaping alone cannot fix this, because `.` and `..` are dot segments that a path +resolver strips whether or not they are encoded, and an empty value addresses the +collection rather than one resource. Values that could change which endpoint is +addressed are therefore rejected rather than escaped: `/`, `?`, `#`, control +characters, `.`, `..` and the empty string now raise an error instead of producing a +request that quietly 404s. Everything else is escaped as before. + +No valid GoCardless resource identity contains any of these characters, so correct +code is unaffected. Ruby and Java previously encoded `/` as `%2F` and sent the +request; they now raise. + ## 3.17.0 (2026-10-05) ### Features diff --git a/gocardless_pro/__init__.py b/gocardless_pro/__init__.py index dccfaf63..14883fab 100644 --- a/gocardless_pro/__init__.py +++ b/gocardless_pro/__init__.py @@ -2,5 +2,5 @@ from .client import Client -__version__ = '3.17.0' +__version__ = '3.18.0' diff --git a/gocardless_pro/api_client.py b/gocardless_pro/api_client.py index c2b82504..3227757f 100644 --- a/gocardless_pro/api_client.py +++ b/gocardless_pro/api_client.py @@ -192,7 +192,7 @@ def _default_headers(self): 'Authorization': 'Bearer {0}'.format(self.access_token), 'Content-Type': 'application/json', 'GoCardless-Client-Library': 'gocardless-pro-python', - 'GoCardless-Client-Version': '3.17.0', + 'GoCardless-Client-Version': '3.18.0', 'User-Agent': self._user_agent(), 'GoCardless-Version': '2015-07-06', } @@ -201,7 +201,7 @@ def _user_agent(self): python_version = '.'.join(platform.python_version_tuple()[0:2]) vm_version = '{}.{}.{}-{}{}'.format(*sys.version_info) return ' '.join([ - 'gocardless-pro-python/3.17.0', + 'gocardless-pro-python/3.18.0', 'python/{0}'.format(python_version), '{0}/{1}'.format(platform.python_implementation(), vm_version), '{0}/{1}'.format(platform.system(), platform.release()), diff --git a/setup.py b/setup.py index c35cfbec..269bee93 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name = 'gocardless_pro', - version = '3.17.0', + version = '3.18.0', packages = find_packages(exclude=['tests']), install_requires = ['requests>=2.34.2'], python_requires = '>=3.6',