@google-cloud/common@8.0.2 security patch breaks ADC project ID detection — joinURIComponents() encodes `DEFAULT_PROJECT_ID_TOKEN` before` replaceProjectIdToken()` runs · Issue #9256 · googleapis/google-cloud-node · GitHub
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

Open

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.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

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

  1. changed the title [-]@google-cloud/common@8.0.2 security patch breaks ADC project ID detection — joinURIComponents() encodes DEFAULT_PROJECT_ID_TOKEN before replaceProjectIdToken() runs[/-] [+]@google-cloud/common@8.0.2 security patch breaks ADC project ID detection — joinURIComponents() encodes `DEFAULT_PROJECT_ID_TOKEN` before` replaceProjectIdToken()` runs[/+] on Sep 7, 2026
  2. jt274 commented on Sep 10, 2026

    @jt274

    This should be a very high priority issue since it breaks ALL BigQuery operations.

  3. added 5 commits that reference this issue on Sep 11, 2026
    dc2457f
    5bb52a1
    73cc0b5
    a410173
    965e9bd
  4. TorbenWetter commented on Oct 1, 2026

    @TorbenWetter

    Confirming this end to end against the live BigQuery API, and confirming that #9315 fixes it.

    Setup: @google-cloud/bigquery 9.1.0 with @google-cloud/common 8.1.0 (its build/src is identical to 8.0.2, so both are affected), @google-cloud/projectify 6.1.0, Node 24, Application Default Credentials.

    import { BigQuery } from '@google-cloud/bigquery'
    const bq = new BigQuery({ location: 'europe-west3' })   // no projectId, ADC resolves it
    await bq.createQueryJob({ query: 'SELECT 1', dryRun: true, location: 'europe-west3' })

    Result without projectId: HTTP 400 from jobs.insert:

    Value <my-project> in content does not agree with value {{projectId}}. This can happen when a value set through a parameter is inconsistent with a value set in the request.
    

    The request body carries the resolved project (jobReference.projectId), the URL path still carries the encoded placeholder. With projectId passed to the constructor the same call succeeds. For jobs.insert the symptom is this 400 rather than the 404 seen on queries, which may help people searching for the error text.

    With the change from #9315 applied to the installed build/src/util.js (the DEFAULT_PROJECT_ID_TOKEN bypass in joinURIComponents()), the same project-less call succeeds against the live API: createQueryJob returns a job in the resolved project. No other change was needed.

    In our case this reached production because a Renovate-driven bump moved @google-cloud/bigquery from 8.3.1 (common 6) straight to 9.1.0: the API surface was unchanged and a mocked test suite stayed green, so nothing flagged the regression until real requests failed. An explicit projectId is a complete workaround.

  5. added a commit that references this issue on Oct 13, 2026
    66b386b
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