Skip to content
Merged
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
87 changes: 49 additions & 38 deletions __fixtures__/output/utils/astHelpers/inlineNestedObj/path-obj.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,57 @@
* and run the pg-proto-parser generate command to regenerate this file.
*/

const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);

function parsePath(path: string): string[] {
const keys = path.replace(/\[(\w+)\]/g, '.$1').split('.');
for (const key of keys) {
if (UNSAFE_KEYS.has(key)) {
throw new Error('Unsafe path segment: ' + key);
}
}
return keys;
}

export default {
get<T>(obj: Record<string, any>, path: string): T | undefined {
const keys = path.replace(/[(w+)]/g, '.$1').split('.');
let result: any = obj;
for (const key of keys) {
if (result == null) {
return undefined;
}
result = result[key];
get<T>(obj: Record<string, any>, path: string): T | undefined {
const keys = parsePath(path);
let result: any = obj;
for (const key of keys) {
if (result == null) {
return undefined;
}
return result as T;
},

set(obj: Record<string, any>, path: string, value: any): void {
if (value === undefined) {
return;
}

const keys = path.replace(/[(w+)]/g, '.$1').split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
result = result[key];
}
return result as T;
},

set(obj: Record<string, any>, path: string, value: any): void {
if (value === undefined) {
return;
}

const keys = parsePath(path);
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (typeof current[key] !== 'object') {
current[key] = {};
}
current[keys[keys.length - 1]] = value;
},

has(obj: Record<string, any>, path: string): boolean {
const keys = path.replace(/[(w+)]/g, '.$1').split('.');
let current = obj;
for (const key of keys) {
if (current == null || !(key in current)) {
return false;
}
current = current[key];
current = current[key];
}
current[keys[keys.length - 1]] = value;
},

has(obj: Record<string, any>, path: string): boolean {
const keys = parsePath(path);
let current = obj;
for (const key of keys) {
if (current == null || !(key in current)) {
return false;
}
return true;
current = current[key];
}
};

return true;
}
};
8 changes: 0 additions & 8 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,6 @@ export default [
'**/__tests__/kitchen-sink/**'
]
},
{
// template literals holding code that is emitted verbatim; changing the
// escaping here would change the generated output
files: ['packages/proto-parser/src/inline-helpers/**'],
rules: {
'no-useless-escape': 'off'
}
},
{
// per-version transformers mirror the AST shape field for field, including
// identity assignments and empty node payloads such as `{ Null: {} }`
Expand Down
87 changes: 49 additions & 38 deletions packages/proto-parser/__tests__/__snapshots__/utils.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -13146,49 +13146,60 @@ export interface ScanToken {
* and run the pg-proto-parser generate command to regenerate this file.
*/

const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);

function parsePath(path: string): string[] {
const keys = path.replace(/\\[(\\w+)\\]/g, '.$1').split('.');
for (const key of keys) {
if (UNSAFE_KEYS.has(key)) {
throw new Error('Unsafe path segment: ' + key);
}
}
return keys;
}

export default {
get<T>(obj: Record<string, any>, path: string): T | undefined {
const keys = path.replace(/[(w+)]/g, '.$1').split('.');
let result: any = obj;
for (const key of keys) {
if (result == null) {
return undefined;
}
result = result[key];
}
return result as T;
},

set(obj: Record<string, any>, path: string, value: any): void {
if (value === undefined) {
return;
get<T>(obj: Record<string, any>, path: string): T | undefined {
const keys = parsePath(path);
let result: any = obj;
for (const key of keys) {
if (result == null) {
return undefined;
}

const keys = path.replace(/[(w+)]/g, '.$1').split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
result = result[key];
}
return result as T;
},

set(obj: Record<string, any>, path: string, value: any): void {
if (value === undefined) {
return;
}

const keys = parsePath(path);
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (typeof current[key] !== 'object') {
current[key] = {};
}
current[keys[keys.length - 1]] = value;
},

has(obj: Record<string, any>, path: string): boolean {
const keys = path.replace(/[(w+)]/g, '.$1').split('.');
let current = obj;
for (const key of keys) {
if (current == null || !(key in current)) {
return false;
}
current = current[key];
current = current[key];
}
current[keys[keys.length - 1]] = value;
},

has(obj: Record<string, any>, path: string): boolean {
const keys = parsePath(path);
let current = obj;
for (const key of keys) {
if (current == null || !(key in current)) {
return false;
}
return true;
current = current[key];
}
};

return true;
}
};
",
"file": "path-obj.ts",
},
Expand Down
16 changes: 16 additions & 0 deletions packages/proto-parser/__tests__/inline-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { nestedObjCode } from '../src/inline-helpers';

describe('inlined nested-obj helper', () => {
it('emits the bracket-notation regex unescaped by the template literal', () => {
expect(nestedObjCode).toContain('path.replace(/\\[(\\w+)\\]/g, \'.$1\')');
});

it('guards every accessor against prototype pollution', () => {
expect(nestedObjCode).toContain(
'const UNSAFE_KEYS = new Set([\'__proto__\', \'constructor\', \'prototype\']);'
);
expect(nestedObjCode).toContain('throw new Error(\'Unsafe path segment: \' + key);');
// get, set and has all route through the validating parser
expect(nestedObjCode.match(/const keys = parsePath\(path\);/g)).toHaveLength(3);
});
});
92 changes: 53 additions & 39 deletions packages/proto-parser/src/inline-helpers/nested-obj.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,59 @@
// Mirrors the published `nested-obj` package (>= 0.2.2), including its
// prototype-pollution guard. Backslashes are doubled so the emitted file
// contains the intended regex.
export const nestedObjCode = `
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);

function parsePath(path: string): string[] {
const keys = path.replace(/\\[(\\w+)\\]/g, '.$1').split('.');
for (const key of keys) {
if (UNSAFE_KEYS.has(key)) {
throw new Error('Unsafe path segment: ' + key);
}
}
return keys;
}

export default {
get<T>(obj: Record<string, any>, path: string): T | undefined {
const keys = path.replace(/\[(\w+)\]/g, '.$1').split('.');
let result: any = obj;
for (const key of keys) {
if (result == null) {
return undefined;
}
result = result[key];
}
return result as T;
},

set(obj: Record<string, any>, path: string, value: any): void {
if (value === undefined) {
return;
get<T>(obj: Record<string, any>, path: string): T | undefined {
const keys = parsePath(path);
let result: any = obj;
for (const key of keys) {
if (result == null) {
return undefined;
}

const keys = path.replace(/\[(\w+)\]/g, '.$1').split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
result = result[key];
}
return result as T;
},

set(obj: Record<string, any>, path: string, value: any): void {
if (value === undefined) {
return;
}

const keys = parsePath(path);
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (typeof current[key] !== 'object') {
current[key] = {};
}
current[keys[keys.length - 1]] = value;
},

has(obj: Record<string, any>, path: string): boolean {
const keys = path.replace(/\[(\w+)\]/g, '.$1').split('.');
let current = obj;
for (const key of keys) {
if (current == null || !(key in current)) {
return false;
}
current = current[key];
current = current[key];
}
current[keys[keys.length - 1]] = value;
},

has(obj: Record<string, any>, path: string): boolean {
const keys = parsePath(path);
let current = obj;
for (const key of keys) {
if (current == null || !(key in current)) {
return false;
}
return true;
current = current[key];
}
};

`;
return true;
}
};
`;
Loading