Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions core/common/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1158,14 +1158,20 @@ export function encodeAbsoluteURI(uri: string): string {
* Trims slashes, encodes path segments to prevent path traversal, and joins
* URI components into a single relative path.
*
* DEFAULT_PROJECT_ID_TOKEN is an internal constant set by the library, never
* user input, so bypassing encodeURIPath for it does not open a traversal
* path. Encoding it would produce '%7B%7BprojectId%7D%7D' in the URI, which
* replaceProjectIdToken() cannot match — causing 404s for clients that rely
* on lazy / ADC project-ID resolution (regression introduced by #9188).
*
* @param {string[]} components - URI components to encode and join.
* @return {string} The formatted and joined URI path.
*/
export function joinURIComponents(components: string[]): string {
return components
.map(uriComponent => {
const trimSlashesRegex = /^\/*|\/*$/g;
const trimmed = uriComponent.replace(trimSlashesRegex, '');
const trimmed = uriComponent.replace(/^\/*|\/*$/g, '');
if (trimmed === DEFAULT_PROJECT_ID_TOKEN) return trimmed;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using DEFAULT_PROJECT_ID_TOKEN here will cause a compilation error if it is not imported. However, importing it from ./service into ./util introduces a circular dependency (service -> util -> service), which can lead to runtime issues (e.g., DEFAULT_PROJECT_ID_TOKEN being evaluated as undefined during module initialization).

To resolve this cleanly, consider defining DEFAULT_PROJECT_ID_TOKEN in src/util.ts and exporting it, then re-exporting it from src/service.ts to maintain backward compatibility.

return encodeURIPath(trimmed); // Encode and prevent path traversal.
})
.join('/');
Expand Down
15 changes: 15 additions & 0 deletions core/common/test/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,21 @@ describe('Service', () => {
service.request_(reqOpts, assert.ifError);
});

it('should pass DEFAULT_PROJECT_ID_TOKEN unencoded to makeAuthenticatedRequest', done => {
// Regression: #9188 caused {{projectId}} to be encoded as
// %7B%7BprojectId%7D%7D, so replaceProjectIdToken() could no longer
// find and substitute it, causing 404s for ADC clients.
const config = extend({}, CONFIG, {projectIdRequired: true});
const service = new Service(config, {}); // no projectId → DEFAULT_PROJECT_ID_TOKEN

service.makeAuthenticatedRequest = (reqOpts_: DecorateRequestOptions) => {
assert.match(reqOpts_.uri, /projects\/\{\{projectId\}\}\//);
done();
};

service.request_({uri: 'queries'}, assert.ifError);
});

it('should use projectId override', done => {
const config = extend({}, CONFIG, {projectIdRequired: true});
const service = new Service(config, OPTIONS);
Expand Down
37 changes: 37 additions & 0 deletions core/common/test/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
ParsedHttpRespMessage,
ParsedHttpResponseBody,
Util,
joinURIComponents,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The test uses replaceProjectIdToken on line 1937, but it is not imported from ../src/util. This will cause a compilation error. Please add replaceProjectIdToken to the imports.

  joinURIComponents,
  replaceProjectIdToken,

} from '../src/util';
import {DEFAULT_PROJECT_ID_TOKEN} from '../src/service';

Expand Down Expand Up @@ -1922,4 +1923,40 @@ describe('common/util', () => {
assert.strictEqual(cb, callback);
});
});

describe('joinURIComponents', () => {
it('should preserve DEFAULT_PROJECT_ID_TOKEN so replaceProjectIdToken can substitute it', () => {
// Regression: #9188 caused {{projectId}} to be encoded as %7B%7BprojectId%7D%7D,
// breaking ADC / lazy project-ID resolution. See #9256.
const joined = joinURIComponents([
'https://bigquery.googleapis.com/bigquery/v2',
'projects',
DEFAULT_PROJECT_ID_TOKEN,
'queries',
]);
const substituted = replaceProjectIdToken(joined, 'my-project');
assert.strictEqual(
substituted,
'https://bigquery.googleapis.com/bigquery/v2/projects/my-project/queries',
);
});

it('should still reject path traversal in non-token components', () => {
assert.throws(() => {
joinURIComponents(['https://example.com/v1', '{{../../admin}}', 'child']);
});
});

it('should encode special characters in non-token path segments', () => {
const result = joinURIComponents([
'https://example.com/v1',
'resource with spaces',
'child',
]);
assert.strictEqual(
result,
'https://example.com/v1/resource%20with%20spaces/child',
);
});
});
});
Loading