Skip to content

fix: Patch the vulnerability for Bigquery and other REST based clients - #9188

Open
danieljbruce wants to merge 36 commits into
mainfrom
vulnerability-bigquery-rest-traversal
Open

fix: Patch the vulnerability for Bigquery and other REST based clients#9188
danieljbruce wants to merge 36 commits into
mainfrom
vulnerability-bigquery-rest-traversal

Conversation

@danieljbruce

@danieljbruce danieljbruce commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Description

This pull request adds URI path validation and RFC 3986 percent-encoding to @google-cloud/common to prevent path traversal and injection vulnerabilities across REST requests. It introduces helper utilities replicated from google-gax and updates Service and ServiceObject request handling to validate and encode path segments safely.

Impact

  • Security Hardening: Rejects path traversal payloads (., .., %2e, %2E, %2e%2e, %2E%2E, foo/../bar) before requests are sent to the network.
  • URI Encoding Compliance: Correctly encodes special characters, fragments (#), and query parameter (?) injection payloads in resource IDs while preserving valid colon-separated resource names and pre-encoded path segments without double encoding or decoding.

Testing

  • Common Library Unit Tests: Added and verified 12 test cases in core/common/test/traversal.ts testing both ServiceObject and Service across all traversal and encoding scenarios (all passing).
  • BigQuery Verification: Built and verified compilation of @google-cloud/bigquery with the new system test suite in handwritten/bigquery/system-test/traversal.ts.

Next Steps

  • Release and publish the updated @google-cloud/common package to npm.
  • Bump @google-cloud/common dependency in @google-cloud/bigquery.
  • Set up a mock server for BigQuery testing to verify outgoing REST request paths end-to-end and add tests against the Bigquery service.
  • Unskip (describe.skip -> describe) and run the BigQuery system tests in handwritten/bigquery/system-test/traversal.ts.
  • Delete the temporary test suite in core/common/test/traversal.ts once the downstream tests are active.

@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 URI encoding and path traversal validation utilities to secure service requests against directory traversal attacks. However, the security review identified critical vulnerabilities where percent-encoded sequences (e.g., %2e%2e) can bypass the validation checks in validateUriPathSegment and validateUriPath. Additionally, encodeURIPath may double-encode already percent-encoded segments. To address these issues, it is recommended to decode path segments using decodeURIComponent before performing validation and encoding.

Comment thread core/common/src/util.ts
Comment on lines +1029 to +1033
export function validateUriPathSegment(propertyName: string, value: string): void {
if (value === '.' || value === '..') {
throw new Error(`Invalid value ${value} for ${propertyName}`);
}
}

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.

security-high high

validateUriPathSegment does not decode the input segment before checking if it is . or ... If a user passes a percent-encoded path traversal sequence (e.g., %2e%2e), this check will be bypassed. To prevent path traversal bypasses, the segment should be decoded using decodeURIComponent before validation.

export function validateUriPathSegment(propertyName: string, value: string): void {
  try {
    const decoded = decodeURIComponent(value);
    if (decoded === '.' || decoded === '..') {
      throw new Error(`Invalid value ${value} for ${propertyName}`);
    }
  } catch {
    if (value === '.' || value === '..') {
      throw new Error(`Invalid value ${value} for ${propertyName}`);
    }
  }
}

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.

Now it does decode the uri before doing the validation, but I think it is also worth noting here that we needed to carefully consider the impact of decoding uris on existing use cases since it means that the Bigquery server will be receiving a different uri which is also the case when encoding anyway. In consideration of the tradeoffs involved, I think decoding is worth it and for the sake of time we'll stick with it for now, but I'll suggest future work we may want to consider.

  1. We should explore more combinations for tests against the Bigquery service, particularly with :, /, ? and .. plus . characters both pre-encoded and not encoded yet to see what the outcomes are. This involves setting up a test that sets up a dataset so that we can create tests that are expected to make successful API calls.
  2. We could do an investigation with a mock server to see what strings really reach the server to get a understanding of exactly what is decoded and where. Understanding exactly what reaches the server would allow us to cut down on our test cases in datastore: benchmark datastore calls against protobuf endpoint #1 and decide what the outcomes really should be and what is possible.

Comment thread core/common/src/util.ts
Comment on lines +1039 to +1051
export function validateUriPath(propertyName: string, value: string): void {
if (value) {
// Split by slash and check for exact segment matches of '.' or '..' rather
// than using a simple string.includes('.') check. This avoids rejecting
// valid domain-scoped resource segments (e.g. projects/example.com:project-id).
const segments = value.split('/');
if (segments.some(segment => segment === '.' || segment === '..')) {
throw new Error(
`Value for ${propertyName} must not contain segments that are exactly . or ..`,
);
}
}
}

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.

security-high high

validateUriPath does not decode the path segments before checking for . or ... A user could bypass this check by using percent-encoded characters (e.g., %2e%2e). To ensure robust protection against path traversal, each segment should be decoded using decodeURIComponent before checking.

export function validateUriPath(propertyName: string, value: string): void {
  if (value) {
    const segments = value.split('/');
    const hasTraversal = segments.some(segment => {
      try {
        const decoded = decodeURIComponent(segment);
        return decoded === '.' || decoded === '..';
      } catch {
        return segment === '.' || segment === '..';
      }
    });
    if (hasTraversal) {
      throw new Error(
        `Value for ${propertyName} must not contain segments that are exactly . or ..`,
      );
    }
  }
}

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.

This is basically saying we should decode the path segments which the code now does.

Comment thread core/common/src/util.ts
Comment on lines +1092 to +1115
export function encodeURIPath(uri: string): string {
const parts = uri.split('/');
return parts
.map(part => {
if (part === '') {
return '';
}
if (part.includes(':')) {
const subparts = part.split(':');
return subparts
.map(subpart => {
if (subpart === '') {
return '';
}
validateUriPathSegment('path segment', subpart);
return encodeWithSlashes(subpart);
})
.join(':');
}
validateUriPathSegment('path segment', part);
return encodeWithSlashes(part);
})
.join('/');
}

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.

security-high high

encodeURIPath currently double-encodes any path segments that are already percent-encoded (e.g., %20 becomes %2520). This will break legitimate requests that contain pre-encoded characters (such as spaces or non-ASCII characters in table/dataset IDs). Additionally, if the input is already percent-encoded, the path traversal checks in validateUriPathSegment can be bypassed.

To fix both issues, each segment/subpart should be decoded using decodeURIComponent before validation and encoding.

export function encodeURIPath(uri: string): string {
  const parts = uri.split('/');
  return parts
    .map(part => {
      if (part === '') {
        return '';
      }
      if (part.includes(':')) {
        const subparts = part.split(':');
        return subparts
          .map(subpart => {
            if (subpart === '') {
              return '';
            }
            let decoded = subpart;
            try {
              decoded = decodeURIComponent(subpart);
            } catch {
              // Fallback to raw subpart if decoding fails
            }
            validateUriPathSegment('path segment', decoded);
            return encodeWithSlashes(decoded);
          })
          .join(':');
      }
      let decoded = part;
      try {
        decoded = decodeURIComponent(part);
      } catch {
        // Fallback to raw part if decoding fails
      }
      validateUriPathSegment('path segment', decoded);
      return encodeWithSlashes(decoded);
    })
    .join('/');
}

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.

As discussed in other comments, we do the decoding now so I think this has been addressed.

@danieljbruce danieljbruce changed the title Vulnerability bigquery rest traversal fix: Patch the vulnerability for Bigquery and other REST based clients Aug 24, 2026
@danieljbruce
danieljbruce marked this pull request as ready for review August 24, 2026 17:16
@danieljbruce
danieljbruce requested review from a team as code owners August 24, 2026 17:16
@github-actions
github-actions Bot requested a review from westarle August 24, 2026 17:17
@danieljbruce

Copy link
Copy Markdown
Contributor Author

I tried fixing the linting issues with AI, but it caused regressions and the linting issues are mostly unrelated to the pull request anyway. I don't suggest we block this on the presubmit / lint check. If we really want to address the linting issues we should do so in a separate PR.

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

Nothing terribly bad, just a few suggestion comments.

Comment thread core/common/src/util.ts
* @param {string} propertyName - The name of the property being validated.
* @param {string} value - The segment value to validate.
*/
export function validateUriPathSegment(propertyName: string, value: string): void {

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 agree with you on most of the linter errors, maybe take a look at this one, though, it's in new code and looks straightforward.

Comment thread core/common/src/util.ts
* This segment-by-segment check prevents directory traversal while allowing
* legitimate resource names containing dots (e.g., domain-scoped project IDs).
*
* This method is a replica of the method found in Google GAX (google-gax).

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.

Maybe not for this PR, but we might want to consider factoring this out one way or another. (Maybe putting it here and importing it in gax.)

expectedError: /Invalid value \.\. for path segment/,
},
{
description: 'should reject paths containing dot-dot segment (foo/../bar)',

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.

Probably also do this lint fix.

// See the License for the specific language governing permissions and
// limitations under the License.

// TODO: Delete this test suite after the traversal tests in

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.

It feels like we ought to keep unit tests next to the things being tested, especially if we end up later refactoring gax on this. Maybe not these specific ones, if they're BQ-specific, but I'm not sure we should delete it.

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