googleapis / googleapis/google-cloud-node
@google-cloud/common@8.0.2 security patch breaks ADC project ID detection — joinURIComponents() encodes `DEFAULT_PROJECT_ID_TOKEN` before` replaceProjectIdToken()` runs
- Dominant language
- TypeScript
- Stars
- 3.2k
- Forks
- 712
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 99
Description
## Summary
`@google-cloud/common@8.0.2` introduced `joinURIComponents()` in `Service.request_()` as part of PR #9188.
While encoding URI path components is appropriate for user-supplied resource IDs, it also encodes the internal `DEFAULT_PROJECT_ID_TOKEN` (`{{projectId}}`).
That token is expected to remain unchanged until `decorateRequest()` calls `replaceProjectIdToken()` and substitutes the actual project ID.
As a result, clients relying on lazy project ID resolution / ADC — for example:
```js
new BigQuery()
```
without an explicit `projectId` — may send the encoded literal:
```text
%7B%7BprojectId%7D%7D
```
to the backend instead of the resolved project ID.
## Regression
### `@google-cloud/common@8.0.1`
```text
request_()
→ URI: ".../projects/{{projectId}}/queries"
decorateRequest()
→ replaceProjectIdToken(uri, "my-project")
→ placeholder is found and replaced
BigQuery receives:
".../projects/my-project/queries" ✓
```
### `@google-cloud/common@8.0.2`
```text
request_()
→ joinURIComponents()
→ "{{projectId}}" becomes "%7B%7BprojectId%7D%7D"
→ URI: ".../projects/%7B%7BprojectId%7D%7D/queries"
decorateRequest()
→ replaceProjectIdToken(uri, "my-project")
→ "{{projectId}}" is no longer present
BigQuery receives:
".../projects/%7B%7BprojectId%7D%7D/queries" ✗
Result: 404
```
## Minimal reproduction
`makeAuthenticatedRequest` is assigned as an instance property, so it needs to be replaced after constructing the `Service`.
The following reproduction captures the URI passed to `makeAuthenticatedRequest`. This allows the behavior to be reproduced without depending on ADC credentials, network access, or a GCP environment.
```js
const {Service} = require('@google-cloud/common');
const svc = new Service(
{
baseUrl: 'https://bigquery.googleapis.com/bigquery/v2',
scopes: ['https://www.googleapis.com/auth/bigquery'],
packageJson: {
name: '@google-cloud/bigquery',
version: '9.0.3',
},
},
{}, // no explicit projectId → DEFAULT_PROJECT_ID_TOKEN
);
let capturedUri;
svc.makeAuthenticatedRequest = reqOpts => {
capturedUri = reqOpts.uri;
};
svc.request_({uri: 'queries'}, () => {});
console.log(capturedUri);
```
Observed behavior:
```text
@google-cloud/common@8.0.1
.../projects/{{projectId}}/queries
@google-cloud/common@8.0.2
.../projects/%7B%7BprojectId%7D%7D/queries
```
In `8.0.1`, the placeholder remains available for `replaceProjectIdToken()`.
In `8.0.2`, it has already been encoded by the time request decoration occurs.
## Connection to #9188
PR #9188 mentions that the BigQuery system tests remained skipped:
> Unskip (`describe.skip` → `describe`) and run the BigQuery system tests once the downstream tests are active.
A system test covering BigQuery initialization through ADC without an explicit `projectId` would likely catch this regression.
## Suggested regression tests
### At the `Service.request_()` level
```js
it('should preserve DEFAULT_PROJECT_ID_TOKEN in the URI', done => {
const service = new Service(
{
baseUrl: 'https://example.googleapis.com/v1',
scopes: [],
packageJson: {
name: 'test',
version: '1.0.0',
},
},
{}
);
service.makeAuthenticatedRequest = reqOpts => {
assert.match(reqOpts.uri, /projects\/\{\{projectId\}\}\//);
done();
};
service.request_({uri: 'queries'}, () => {});
});
```
### At the `joinURIComponents()` level
```js
assert.strictEqual(
joinURIComponents(
'https://example.com/projects',
'{{projectId}}',
'queries'
),
'https://example.com/projects/{{projectId}}/queries'
);
```
## Affected scope
This potentially affects clients extending `Service` that:
1. include `DEFAULT_PROJECT_ID_TOKEN` in request paths, and
2. rely on lazy / ADC project ID resolution.
Confirmed affected configuration:
```text
@google-cloud/bigquery@9.0.3
@google-cloud/common@8.0.2
new BigQuery() without an explicit projectId
```
Downgrading `@google-cloud/common` to `8.0.1` restores the previous behavior.
## Workaround
For pnpm:
```json
{
"pnpm": {
"overrides": {
"@google-cloud/bigquery@9>@google-cloud/common": "8.0.1"
}
}
}
```
## Possible fix
One option would be to preserve `DEFAULT_PROJECT_ID_TOKEN` while encoding URI components.
The token is an internal symbolic placeholder rather than a concrete resource ID, and still needs to be visible to `replaceProjectIdToken()` later in the request lifecycle.
Alternatively, `replaceProjectIdToken()` in `@google-cloud/projectify` could recognize both:
```text
{{projectId}}
```
and:
```text
%7B%7BprojectId%7D%7D
```
However, that would make project substitution aware of URI-encoding behavior from another layer.
Preserving the placeholder before request decoration therefore seems preferable.
## Environment
```text
@google-cloud/common: 8.0.2 (affected)
@google-cloud/common: 8.0.1 (working)
@google-cloud/bigquery: 9.0.3
Node.js: >=22
```
Regression appears to have been introduced by #9188.
## Related
* #9188 — introduction of `joinURIComponents()` in `Service.request_()`
* #8678 — `googleapis-common@8.0.2` / `google-auth-library@10.5.0` dependency issue; appears unrelated to this regression
Contributor guide
Research direction
Start at Service.request_(), joinURIComponents(), and the later decorateRequest() call described in the issue. Add the suggested regression coverage at the Service.request_() or joinURIComponents() level, then verify that DEFAULT_PROJECT_ID_TOKEN remains unencoded until project ID replacement and that the existing URI behavior still passes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- gcp, node.js, typescript
- Domain
- api, backend, cloud
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 76/100