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

Open
#9,256 1 comment 4 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.