Skip to content

@google-cloud/common@8.0.2 security patch breaks ADC project ID detection — joinURIComponents() encodes DEFAULT_PROJECT_ID_TOKEN before replaceProjectIdToken() runs #9256

Description

@akiliscodes

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:

new BigQuery()

without an explicit projectId — may send the encoded literal:

%7B%7BprojectId%7D%7D

to the backend instead of the resolved project ID.

Regression

@google-cloud/common@8.0.1

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

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.

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:

@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.skipdescribe) 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

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

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:

@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:

{
  "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:

{{projectId}}

and:

%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

@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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions