Skip to content

fix: validate path parameters and prevent traversal/injection in apiary request encodings - #9181

Merged
danieljbruce merged 39 commits into
mainfrom
apiary-path-traversal-vulnerability
Aug 21, 2026
Merged

danieljbruce merged 39 commits into
mainfrom
apiary-path-traversal-vulnerability

Conversation

@danieljbruce

@danieljbruce danieljbruce commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Description

#9166 resolves a vulnerability for Gapic client libraries, but that same vulnerability exists for apiary libraries which this pull request patches. While this change is not on the apiary library, it is a change for the nodejs-googleapis-common package which all the apiary libraries use to do their requests.

Impact

Removes the potential for an exploit on apiary libraries.

Important Notes About Code Changes

  1. All the methods in core/packages/nodejs-googleapis-common/src/transcoding.ts except validateAndEncodeParams and extractTemplateParams are exact duplicates of the methods in google-gax. It was important to reuse a lot of this logic so that we only have to scrutinize new code that was necessary due to the differences between apiary and gapic clients.
  2. Tests are applied against createAPIRequest to capture the gax equivalent of both * and ** wildcards as well as all special characters.
  3. To verify that apiary actually uses this code, apiary library tests are provided in core/packages/nodejs-googleapis-common/test/test.dialogflow.ts that explain what apiary passes into createAPIRequest so that we can understand how the code works end to end.

Next Steps

  1. With this pull request we now have duplicate code like applyPattern in gax and common. We should find one place for this code to live and reference that place from both libraries.
  2. The pull request removed the normalizePathParams method from this PR to keep it small in 9b02fe8 commit. However, we should add this method in again to improve code quality.
  3. Swap "require.cache[commonPath] =" out when the new changes are released for this client library to avoid a hacky mock.

Tracked here

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces path parameter validation and encoding to prevent directory traversal and injection attacks. It adds a new transcoding.ts module with utility functions to validate single-segment and multi-segment path parameters, percent-encode them strictly according to RFC 3986, and integrate this validation into the API request creation flow. Comprehensive tests are added to verify these security enhancements. The review feedback suggests simplifying the applyPattern function in transcoding.ts by removing redundant regular expression compilation, as the wildcard patterns are limited to '*' and '**'.

Comment on lines +149 to +196
function applyPattern(
pattern: string,
fieldValue: string,
propertyName = 'resource', // Used to provide precise error messages when path validation fails
): string | undefined {
if (!pattern || pattern === '*') {
validateUriPathSegment(propertyName, fieldValue);
return encodeWithSlashes(fieldValue);
}

if (!pattern.includes('*') && pattern !== fieldValue) {
return undefined;
}

// since we're converting the pattern to a regex, make necessary precautions:
const regex = new RegExp(
'^' +
escapeRegExp(pattern)
.replace(/\\\*\\\*/g, '(.+)')
.replace(/\\\*/g, '([^/]+)') +
'$',
);

const match = fieldValue.match(regex);
if (!match) {
return undefined;
}

// Identify the segments and wildcards in pattern to perform validation in order of appearance
const wildcards: string[] = pattern.match(/\*\*|\*/g) || [];

// Check the captured group values.
// Note: The loop below matches the validation loop in google-gax verbatim.
// TODO: Consider refactoring this in the future.
for (let i = 1; i < match.length; i++) {
const groupVal = match[i];
if (groupVal !== undefined && groupVal !== null) {
const wildcardType = wildcards[i - 1];
if (wildcardType === '*') {
validateUriPathSegment(propertyName, groupVal);
} else if (wildcardType === '**') {
validateUriPath(propertyName, groupVal);
}
}
}

return encodeWithoutSlashes(fieldValue);
}

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.

medium

Since extractTemplateParams only ever returns '*' or '**' as the wildcard pattern, the complex regular expression compilation and matching logic in applyPattern is entirely redundant and unreachable.\n\nWe can simplify applyPattern to directly handle '*' and '**' cases. This avoids compiling a new RegExp on every single parameter validation call, significantly improving performance and readability.

function applyPattern(\n  pattern: string,\n  fieldValue: string,\n  propertyName = 'resource',\n): string | undefined {\n  if (pattern === '*') {\n    validateUriPathSegment(propertyName, fieldValue);\n    return encodeWithSlashes(fieldValue);\n  }\n\n  if (pattern === '**') {\n    validateUriPath(propertyName, fieldValue);\n    return encodeWithoutSlashes(fieldValue);\n  }\n\n  return undefined;\n}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes. But we also want this method to be an exact match with the gax method because that makes the solution way easier to understand.

@danieljbruce danieljbruce changed the title Apiary path traversal vulnerability feat!: Update gaxios to minimum Node version of 22. Aug 20, 2026
@danieljbruce danieljbruce changed the title feat!: Update gaxios to minimum Node version of 22. fix: Resolve apiary path traversal exploit Aug 20, 2026
@danieljbruce danieljbruce changed the title fix: Resolve apiary path traversal exploit fix: validate path parameters and prevent traversal/injection in apiary request encodings Aug 20, 2026
@danieljbruce
danieljbruce marked this pull request as ready for review August 20, 2026 15:57
@danieljbruce
danieljbruce requested a review from a team as a code owner August 20, 2026 15:57
@github-actions
github-actions Bot requested a review from westarle August 20, 2026 15:58
@danieljbruce
danieljbruce requested review from westarle and removed request for westarle August 20, 2026 15:58

@westarle westarle left a comment

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.

A couple opportunities to simplify and hopefully avoid bugs. I notice a few lints, also.

Comment on lines +170 to +174
options.url !== undefined && options.url !== null
? typeof options.url === 'object'
? options.url.toString()
: options.url
: parameters.mediaUrl ?? undefined,

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.

could this be extracted like:

const urlTemplateString = options.url?.toString() ?? parameters.mediaUrl ?? undefined;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I mostly agree with this, but with a few adjustments:

  1. We should allow options.url to be used if it is available even if it is not an object that has toString method
  2. I definitely think that inlining here instead of creating a urlTemplateString variable is the right approach with the main benefit being that it doesn't create an extra urlTemplateString variable that the reader has to wonder about in the rest of the method. If the intent of the fragment is unclear then maybe just adding a comment is best.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Actually, my point #1 is irrelevant because a url string has a toString method so that will work either way.

* @param urlTemplate The RFC 6570 URI template string
* @returns Array of parameter names and their associated wildcard pattern
*/
function extractTemplateParams(urlTemplate: string): Array<{

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.

I think this covers some advanced templating that I don't think we use in Apiary.

Consider:

function extractTemplateParams(urlTemplate: string): Array<{
  param: string;
  wildcard: '*' | '**';
}> {
  const paramMap = new Map<string, '*' | '**'>();
  
  // Natively skips {}, {#}, {?}, and {,} by demanding valid variable characters
  const matches = urlTemplate.matchAll(/\{(\+?)([a-zA-Z0-9_$-]+)\}/g);

  for (const match of matches) {
    const wildcard = match[1] === '+' ? '**' : '*';
    const paramName = match[2];
    
    if (wildcard === '**' || !paramMap.has(paramName)) {
      paramMap.set(paramName, wildcard);
    }
  }

  return Array.from(paramMap.entries()).map(([param, wildcard]) => ({
    param, wildcard,
  }));
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sounds good. I've applied this code to simplify the method.

Comment on lines +226 to +230
params[param] = Array.isArray(parameterValue)
? parameterValue.map(item =>
applyPattern(wildcard, String(item), param),
)
: applyPattern(wildcard, String(parameterValue), param);

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.

Can we replace with something like this and delete applyPattern:

const encodeParam = (val: string) => {
        validateUriPath(param, val);
        return encodeWithoutSlashes(val);
      };
      params[param] = Array.isArray(parameterValue)
        ? parameterValue.map(item => encodeParam(String(item)))
        : encodeParam(String(parameterValue));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes. That's a good idea, but I've made sure to leave a comment that this achieves what applyPattern from gax achieves because I think pointing out the connection between the two is really important.

@danieljbruce
danieljbruce merged commit b5b5afb into main Aug 21, 2026
43 of 44 checks passed
@danieljbruce
danieljbruce deleted the apiary-path-traversal-vulnerability branch August 21, 2026 17:09
@release-please release-please Bot mentioned this pull request Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants