Skip to content

Commit d4df5b8

Browse files
committed
Reimplement dangerous request parameter check with visitor
1 parent f698b45 commit d4df5b8

File tree

9 files changed

+186
-102
lines changed

9 files changed

+186
-102
lines changed

.changeset/chilled-lamps-hammer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/service-sync-rules': patch
3+
---
4+
5+
Remove `usesAuthenticatedRequestParameters` and `usesUnauthenticatedRequestParameters` in favor of `DetectRequestParameters` computing them on-demand.

packages/sync-rules/src/SqlParameterQuery.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
SqliteRow
3131
} from './types.js';
3232
import { filterJsonRow, getBucketId, isJsonValue, isSelectStatement, normalizeParameterValue } from './utils.js';
33+
import { DetectRequestParameters } from './validators.js';
3334

3435
export interface SqlParameterQueryOptions {
3536
sourceTable: TablePattern;
@@ -496,30 +497,29 @@ export class SqlParameterQuery {
496497

497498
get hasAuthenticatedBucketParameters(): boolean {
498499
// select request.user_id() as user_id where ...
499-
const authenticatedExtractor =
500-
Object.values(this.parameterExtractors).find(
501-
(clause) => isParameterValueClause(clause) && clause.usesAuthenticatedRequestParameters
502-
) != null;
503-
return authenticatedExtractor;
500+
const visitor = new DetectRequestParameters();
501+
visitor.acceptAll(Object.values(this.parameterExtractors));
502+
503+
return visitor.usesAuthenticatedRequestParameters;
504504
}
505505

506506
get hasAuthenticatedMatchClause(): boolean {
507507
// select ... where user_id = request.user_id()
508-
const authenticatedInputParameter = this.filter.usesAuthenticatedRequestParameters;
509-
return authenticatedInputParameter;
508+
const visitor = new DetectRequestParameters();
509+
visitor.accept(this.filter);
510+
return visitor.usesAuthenticatedRequestParameters;
510511
}
511512

512513
get usesUnauthenticatedRequestParameters(): boolean {
514+
const visitor = new DetectRequestParameters();
515+
513516
// select ... where request.parameters() ->> 'include_comments'
514-
const unauthenticatedInputParameter = this.filter.usesUnauthenticatedRequestParameters;
517+
visitor.accept(this.filter);
515518

516519
// select request.parameters() ->> 'project_id'
517-
const unauthenticatedExtractor =
518-
Object.values(this.parameterExtractors).find(
519-
(clause) => isParameterValueClause(clause) && clause.usesUnauthenticatedRequestParameters
520-
) != null;
520+
visitor.acceptAll(Object.values(this.parameterExtractors));
521521

522-
return unauthenticatedInputParameter || unauthenticatedExtractor;
522+
return visitor.usesUnauthenticatedRequestParameters;
523523
}
524524

525525
/**

packages/sync-rules/src/StaticSqlParameterQuery.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
SqliteJsonValue
1212
} from './types.js';
1313
import { getBucketId, isJsonValue } from './utils.js';
14+
import { DetectRequestParameters } from './validators.js';
1415

1516
export interface StaticSqlParameterQueryOptions {
1617
sql: string;
@@ -194,26 +195,21 @@ export class StaticSqlParameterQuery {
194195
// select where request.jwt() ->> 'role' == 'authorized'
195196
// we do not count this as a sufficient check
196197
// const authenticatedFilter = this.filter.usesAuthenticatedRequestParameters;
198+
const visitor = new DetectRequestParameters();
199+
visitor.acceptAll(Object.values(this.parameterExtractors));
197200

198-
// select request.user_id() as user_id
199-
const authenticatedExtractor =
200-
Object.values(this.parameterExtractors).find(
201-
(clause) => isParameterValueClause(clause) && clause.usesAuthenticatedRequestParameters
202-
) != null;
203-
return authenticatedExtractor;
201+
return visitor.usesAuthenticatedRequestParameters;
204202
}
205203

206204
get usesUnauthenticatedRequestParameters(): boolean {
205+
const visitor = new DetectRequestParameters();
206+
207207
// select where request.parameters() ->> 'include_comments'
208-
const unauthenticatedFilter = this.filter?.usesUnauthenticatedRequestParameters;
208+
visitor.accept(this.filter);
209209

210210
// select request.parameters() ->> 'project_id'
211-
const unauthenticatedExtractor =
212-
Object.values(this.parameterExtractors).find(
213-
(clause) => isParameterValueClause(clause) && clause.usesUnauthenticatedRequestParameters
214-
) != null;
215-
216-
return unauthenticatedFilter || unauthenticatedExtractor;
211+
visitor.acceptAll(Object.values(this.parameterExtractors));
212+
return visitor.usesUnauthenticatedRequestParameters;
217213
}
218214

219215
get usesDangerousRequestParameters() {

packages/sync-rules/src/TableValuedFunctionSqlParameterQuery.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from './types.js';
1515
import { getBucketId, isJsonValue } from './utils.js';
1616
import { BucketDescription, BucketPriority, DEFAULT_BUCKET_PRIORITY } from './BucketDescription.js';
17+
import { DetectRequestParameters } from './validators.js';
1718

1819
export interface TableValuedFunctionSqlParameterQueryOptions {
1920
sql: string;
@@ -260,37 +261,46 @@ export class TableValuedFunctionSqlParameterQuery {
260261
};
261262
}
262263

264+
private visitParameterExtractorsAndCallClause(): DetectRequestParameters {
265+
const visitor = new DetectRequestParameters();
266+
267+
// e.g. select request.user_id() as user_id
268+
visitor.acceptAll(Object.values(this.parameterExtractors));
269+
270+
// e.g. select value from json_each(request.jwt() ->> 'project_ids')
271+
visitor.accept(this.callClause);
272+
273+
return visitor;
274+
}
275+
263276
get hasAuthenticatedBucketParameters(): boolean {
264277
// select where request.jwt() ->> 'role' == 'authorized'
265278
// we do not count this as a sufficient check
266279
// const authenticatedFilter = this.filter.usesAuthenticatedRequestParameters;
280+
const visitor = new DetectRequestParameters();
267281

268282
// select request.user_id() as user_id
269-
const authenticatedExtractor =
270-
Object.values(this.parameterExtractors).find(
271-
(clause) => isParameterValueClause(clause) && clause.usesAuthenticatedRequestParameters
272-
) != null;
283+
visitor.acceptAll(Object.values(this.parameterExtractors));
273284

274285
// select value from json_each(request.jwt() ->> 'project_ids')
275-
const authenticatedArgument = this.callClause?.usesAuthenticatedRequestParameters ?? false;
286+
visitor.accept(this.callClause);
276287

277-
return authenticatedExtractor || authenticatedArgument;
288+
return visitor.usesAuthenticatedRequestParameters;
278289
}
279290

280291
get usesUnauthenticatedRequestParameters(): boolean {
292+
const visitor = new DetectRequestParameters();
293+
281294
// select where request.parameters() ->> 'include_comments'
282-
const unauthenticatedFilter = this.filter?.usesUnauthenticatedRequestParameters;
295+
visitor.accept(this.filter);
283296

284297
// select request.parameters() ->> 'project_id'
285-
const unauthenticatedExtractor =
286-
Object.values(this.parameterExtractors).find(
287-
(clause) => isParameterValueClause(clause) && clause.usesUnauthenticatedRequestParameters
288-
) != null;
298+
visitor.acceptAll(Object.values(this.parameterExtractors));
289299

290300
// select value from json_each(request.parameters() ->> 'project_ids')
291-
const unauthenticatedArgument = this.callClause?.usesUnauthenticatedRequestParameters ?? false;
301+
visitor.accept(this.callClause);
292302

293-
return unauthenticatedFilter || unauthenticatedExtractor || unauthenticatedArgument;
303+
return visitor.usesUnauthenticatedRequestParameters;
294304
}
295305

296306
get usesDangerousRequestParameters() {

packages/sync-rules/src/request_functions.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ExpressionType } from './ExpressionType.js';
22
import { CompatibilityContext, CompatibilityEdition, CompatibilityOption } from './compatibility.js';
33
import { generateSqlFunctions } from './sql_functions.js';
4-
import { ParameterValueSet, SqliteValue } from './types.js';
4+
import { CompiledClause, ParameterValueClause, ParameterValueSet, SqliteValue } from './types.js';
55

66
export interface SqlParameterFunction {
77
readonly debugName: string;
@@ -131,3 +131,14 @@ const REQUEST_FUNCTIONS_NAMED = {
131131
};
132132

133133
export const REQUEST_FUNCTIONS: Record<string, SqlParameterFunction> = REQUEST_FUNCTIONS_NAMED;
134+
135+
/**
136+
* A {@link ParameterValueClause} derived from a call to a {@link SqlParameterFunction}.
137+
*/
138+
export interface RequestFunctionCall extends ParameterValueClause {
139+
function: SqlParameterFunction;
140+
}
141+
142+
export function isRequestFunctionCall(clause: CompiledClause): clause is RequestFunctionCall {
143+
return (clause as RequestFunctionCall).function != null;
144+
}

packages/sync-rules/src/sql_filters.ts

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { nil } from 'pgsql-ast-parser/src/utils.js';
44
import { BucketPriority, isValidPriority } from './BucketDescription.js';
55
import { ExpressionType } from './ExpressionType.js';
66
import { SqlRuleError } from './errors.js';
7-
import { REQUEST_FUNCTIONS, SqlParameterFunction } from './request_functions.js';
7+
import { REQUEST_FUNCTIONS, RequestFunctionCall, SqlParameterFunction } from './request_functions.js';
88
import {
99
BASIC_OPERATORS,
1010
OPERATOR_IN,
@@ -35,6 +35,7 @@ import {
3535
ClauseError,
3636
CompiledClause,
3737
InputParameter,
38+
LegacyParameterFromTableClause,
3839
ParameterMatchClause,
3940
ParameterValueClause,
4041
QueryParameters,
@@ -419,14 +420,14 @@ export class SqlTools {
419420

420421
const parameterArguments = compiledArguments as ParameterValueClause[];
421422
return {
423+
function: impl,
422424
key: `${schema}.${fn}(${parameterArguments.map((p) => p.key).join(',')})`,
423425
lookupParameterValue(parameters) {
424426
const evaluatedArgs = parameterArguments.map((p) => p.lookupParameterValue(parameters));
425427
return impl.call(parameters, ...evaluatedArgs);
426428
},
427-
usesAuthenticatedRequestParameters: impl.usesAuthenticatedRequestParameters,
428-
usesUnauthenticatedRequestParameters: impl.usesUnauthenticatedRequestParameters
429-
} satisfies ParameterValueClause;
429+
visitChildren: (v) => parameterArguments.forEach(v)
430+
} satisfies RequestFunctionCall;
430431
}
431432
}
432433

@@ -494,8 +495,7 @@ export class SqlTools {
494495
return { [inputParam.key]: value };
495496
});
496497
},
497-
usesAuthenticatedRequestParameters: leftFilter.usesAuthenticatedRequestParameters,
498-
usesUnauthenticatedRequestParameters: leftFilter.usesUnauthenticatedRequestParameters
498+
visitChildren: (v) => v(leftFilter)
499499
} satisfies ParameterMatchClause;
500500
} else if (
501501
this.supportsExpandingParameters &&
@@ -530,8 +530,7 @@ export class SqlTools {
530530
}
531531
return [{ [inputParam.key]: value }];
532532
},
533-
usesAuthenticatedRequestParameters: rightFilter.usesAuthenticatedRequestParameters,
534-
usesUnauthenticatedRequestParameters: rightFilter.usesUnauthenticatedRequestParameters
533+
visitChildren: (v) => v(rightFilter)
535534
} satisfies ParameterMatchClause;
536535
} else {
537536
// Not supported, return the error previously computed
@@ -579,8 +578,7 @@ export class SqlTools {
579578
return { [inputParam.key]: value };
580579
});
581580
},
582-
usesAuthenticatedRequestParameters: leftFilter.usesAuthenticatedRequestParameters,
583-
usesUnauthenticatedRequestParameters: leftFilter.usesUnauthenticatedRequestParameters
581+
visitChildren: (v) => v(leftFilter)
584582
} satisfies ParameterMatchClause;
585583
} else if (
586584
this.supportsExpandingParameters &&
@@ -622,8 +620,7 @@ export class SqlTools {
622620
return { [inputParam.key]: value };
623621
});
624622
},
625-
usesAuthenticatedRequestParameters: rightFilter.usesAuthenticatedRequestParameters,
626-
usesUnauthenticatedRequestParameters: rightFilter.usesUnauthenticatedRequestParameters
623+
visitChildren: (v) => v(rightFilter)
627624
} satisfies ParameterMatchClause;
628625
} else {
629626
// Not supported, return the error previously computed
@@ -652,8 +649,7 @@ export class SqlTools {
652649

653650
return [{ [inputParam.key]: value }];
654651
},
655-
usesAuthenticatedRequestParameters: otherFilter.usesAuthenticatedRequestParameters,
656-
usesUnauthenticatedRequestParameters: otherFilter.usesUnauthenticatedRequestParameters
652+
visitChildren: (v) => v(otherFilter)
657653
} satisfies ParameterMatchClause;
658654
}
659655

@@ -776,17 +772,16 @@ export class SqlTools {
776772
}
777773
}
778774

779-
private getParameterRefClause(expr: ExprRef): ParameterValueClause {
775+
private getParameterRefClause(expr: ExprRef): LegacyParameterFromTableClause {
780776
const table = AvailableTable.search(expr.table?.name ?? this.defaultTable!, this.parameterTables)!.nameInSchema;
781777
const column = expr.name;
782778
return {
779+
table,
783780
key: `${table}.${column}`,
784781
lookupParameterValue: (parameters) => {
785782
return parameters.lookup(table, column);
786-
},
787-
usesAuthenticatedRequestParameters: table == 'token_parameters',
788-
usesUnauthenticatedRequestParameters: table == 'user_parameters'
789-
} satisfies ParameterValueClause;
783+
}
784+
} satisfies LegacyParameterFromTableClause;
790785
}
791786

792787
refHasSchema(ref: ExprRef) {
@@ -861,12 +856,7 @@ export class SqlTools {
861856
} else if (argsType == 'param') {
862857
const argStrings = argClauses.map((e) => (e as ParameterValueClause).key);
863858
const name = `${fnImpl.debugName}(${argStrings.join(',')})`;
864-
const usesAuthenticatedRequestParameters =
865-
argClauses.find((clause) => isParameterValueClause(clause) && clause.usesAuthenticatedRequestParameters) !=
866-
null;
867-
const usesUnauthenticatedRequestParameters =
868-
argClauses.find((clause) => isParameterValueClause(clause) && clause.usesUnauthenticatedRequestParameters) !=
869-
null;
859+
870860
return {
871861
key: name,
872862
lookupParameterValue: (parameters) => {
@@ -881,8 +871,7 @@ export class SqlTools {
881871
});
882872
return fnImpl.call(...args);
883873
},
884-
usesAuthenticatedRequestParameters,
885-
usesUnauthenticatedRequestParameters
874+
visitChildren: (v) => argClauses.forEach(v)
886875
} satisfies ParameterValueClause;
887876
} else {
888877
throw new Error('unreachable condition');
@@ -983,9 +972,7 @@ function staticValueClause(value: SqliteValue): StaticValueClause {
983972
key: JSONBig.stringify(value),
984973
lookupParameterValue(_parameters) {
985974
return value;
986-
},
987-
usesAuthenticatedRequestParameters: false,
988-
usesUnauthenticatedRequestParameters: false
975+
}
989976
};
990977
}
991978

0 commit comments

Comments
 (0)