Skip to content

Commit ee6a082

Browse files
committed
fix(orchestrator): Address CVE-2026-3118 (#2597)
* fix: Update grapql client * Filters, pagination and queries now use query variables fixes CVE-2026-3118 and relates to JIRA https://redhat.atlassian.net/browse/RHIDP-12388 and https://redhat.atlassian.net/browse/RHIDP-12583
1 parent 9544711 commit ee6a082

File tree

11 files changed

+681
-242
lines changed

11 files changed

+681
-242
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-orchestrator-backend': patch
3+
---
4+
5+
- Update dependecy @urql/core to fix CVE-2026-3118
6+
- Reworks the filter and query builder code to use query variables

workspaces/orchestrator/plugins/orchestrator-backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
"@backstage/plugin-scaffolder-node": "^0.12.1",
7777
"@red-hat-developer-hub/backstage-plugin-orchestrator-common": "workspace:^",
7878
"@red-hat-developer-hub/backstage-plugin-orchestrator-node": "workspace:^",
79-
"@urql/core": "^4.1.4",
79+
"@urql/core": "^6.0.1",
8080
"ajv-formats": "^2.1.1",
8181
"cloudevents": "^8.0.0",
8282
"express": "^4.21.2",

workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts

Lines changed: 105 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ import {
2424
TypeName,
2525
} from '@red-hat-developer-hub/backstage-plugin-orchestrator-common';
2626

27+
import { randomBytes } from 'node:crypto';
28+
29+
import { FilterClause, FilterClauseVariable } from '../types/filterClause';
30+
2731
type ProcessType = 'ProcessDefinition' | 'ProcessInstance';
2832

2933
const supportedOperators = [
@@ -73,44 +77,88 @@ function handleLogicalFilter(
7377
introspection: IntrospectionField[],
7478
type: ProcessType,
7579
filter: LogicalFilter,
76-
): string {
77-
if (!filter.operator) return '';
80+
): FilterClause {
81+
if (!filter.operator) return {} as FilterClause;
7882

7983
const subClauses = filter.filters.map(f =>
8084
buildFilterCondition(introspection, type, f),
8185
);
8286

83-
return `${filter.operator.toLowerCase()}: {${subClauses.join(', ')}}`;
87+
const filterClause: FilterClause = {
88+
clause: `${filter.operator.toLowerCase()}: {${subClauses.map(cl => cl.clause).join(', ')}}`,
89+
clauseVariable: subClauses.flatMap(cl => cl.clauseVariable),
90+
};
91+
return filterClause;
8492
}
8593

8694
function handleNestedFilter(
8795
introspection: IntrospectionField[],
8896
type: ProcessType,
8997
filter: NestedFilter,
90-
): string {
98+
): FilterClause {
9199
const subClauses = buildFilterCondition(
92100
introspection,
93101
type,
94102
filter.nested,
95103
true,
96104
);
97105

98-
return `${filter.field}: {${subClauses}}`;
106+
const filterClause: FilterClause = {
107+
clauseVariable: subClauses.clauseVariable,
108+
clause: `${filter.field}: {${subClauses.clause}}`,
109+
};
110+
111+
return filterClause;
99112
}
100113

101-
function handleBetweenOperator(filter: FieldFilter): string {
114+
function handleBetweenOperator(filter: FieldFilter): FilterClause {
102115
if (!Array.isArray(filter.value) || filter.value.length !== 2) {
103116
throw new Error('Between operator requires an array of two elements');
104117
}
105-
return `${filter.field}: {${getGraphQLOperator(
118+
const filterClauseVariableArray: FilterClauseVariable[] = [];
119+
const clauseVariableName1 = `clauseVariable${nonSecureRandomAlphaNumeric()}`;
120+
const filterClauseVariable1: FilterClauseVariable = {
121+
clauseVariableName: clauseVariableName1,
122+
formattedValue: filter.value[0],
123+
clauseVariableType: 'String',
124+
};
125+
126+
const clauseVariableName2 = `clauseVariable${nonSecureRandomAlphaNumeric()}`;
127+
const filterClauseVariable2: FilterClauseVariable = {
128+
clauseVariableName: clauseVariableName2,
129+
formattedValue: filter.value[1],
130+
clauseVariableType: 'String',
131+
};
132+
133+
const clause = `${filter.field}: {${getGraphQLOperator(
106134
FieldFilterOperatorEnum.Between,
107-
)}: {from: "${filter.value[0]}", to: "${filter.value[1]}"}}`;
135+
)}: {from: $${clauseVariableName1}, to: $${clauseVariableName2}}}`;
136+
filterClauseVariableArray.push(filterClauseVariable1, filterClauseVariable2);
137+
const filterClause: FilterClause = {
138+
clause: clause,
139+
clauseVariable: filterClauseVariableArray,
140+
};
141+
142+
return filterClause;
108143
}
109144

110-
function handleIsNullOperator(filter: FieldFilter): string {
111-
return `${filter.field}: {${getGraphQLOperator(
112-
FieldFilterOperatorEnum.IsNull,
113-
)}: ${convertToBoolean(filter.value)}}`;
145+
function handleIsNullOperator(filter: FieldFilter): FilterClause {
146+
const clauseVariableName = `clauseVariable${nonSecureRandomAlphaNumeric()}`;
147+
const clause = `${filter.field}: {${getGraphQLOperator(FieldFilterOperatorEnum.IsNull)}: $${clauseVariableName}}`;
148+
149+
const filterClauseVariable: FilterClauseVariable = {
150+
clauseVariableName: clauseVariableName,
151+
formattedValue: convertToBoolean(filter.value),
152+
clauseVariableType: 'Boolean',
153+
};
154+
const filterClauseVariableArray: FilterClauseVariable[] = [];
155+
filterClauseVariableArray.push(filterClauseVariable);
156+
const clauseObject: FilterClause = {
157+
clauseVariable: filterClauseVariableArray,
158+
clause,
159+
};
160+
161+
return clauseObject;
114162
}
115163

116164
function isEnumFilter(
@@ -136,32 +184,58 @@ function handleBinaryOperator(
136184
binaryFilter: FieldFilter,
137185
fieldDef: IntrospectionField | undefined,
138186
type: 'ProcessDefinition' | 'ProcessInstance',
139-
): string {
187+
): FilterClause {
140188
if (isEnumFilter(binaryFilter.field, type)) {
141189
if (!isValidEnumOperator(binaryFilter.operator)) {
142190
throw new Error(
143191
`Invalid operator ${binaryFilter.operator} for enum field ${binaryFilter.field} filter`,
144192
);
145193
}
146194
}
147-
const formattedValue = Array.isArray(binaryFilter.value)
148-
? `[${binaryFilter.value
149-
.map(v => formatValue(binaryFilter.field, v, fieldDef, type))
150-
.join(', ')}]`
151-
: formatValue(binaryFilter.field, binaryFilter.value, fieldDef, type);
152-
return `${binaryFilter.field}: {${getGraphQLOperator(
153-
binaryFilter.operator,
154-
)}: ${formattedValue}}`;
195+
let formattedValue: any;
196+
let paramType: string;
197+
if (Array.isArray(binaryFilter.value)) {
198+
formattedValue = binaryFilter.value.map(v =>
199+
formatValue(binaryFilter.field, v, fieldDef, type),
200+
);
201+
paramType = isEnumFilter(binaryFilter.field, type)
202+
? '[ProcessInstanceState!]'
203+
: '[String!]';
204+
} else {
205+
formattedValue = formatValue(
206+
binaryFilter.field,
207+
binaryFilter.value,
208+
fieldDef,
209+
type,
210+
);
211+
paramType = 'String';
212+
}
213+
214+
const clauseVariableName = `clauseVariable${nonSecureRandomAlphaNumeric()}`;
215+
const clause = `${binaryFilter.field}: {${getGraphQLOperator(binaryFilter.operator)}: $${clauseVariableName}}`;
216+
const filterClauseVariable: FilterClauseVariable = {
217+
clauseVariableName: clauseVariableName,
218+
formattedValue: formattedValue,
219+
clauseVariableType: paramType,
220+
};
221+
const filterClauseVariableArray: FilterClauseVariable[] = [];
222+
filterClauseVariableArray.push(filterClauseVariable);
223+
const clauseObject: FilterClause = {
224+
clauseVariable: filterClauseVariableArray,
225+
clause,
226+
};
227+
228+
return clauseObject;
155229
}
156230

157231
export function buildFilterCondition(
158232
introspection: IntrospectionField[],
159233
type: ProcessType,
160234
filters?: Filter,
161235
isNested?: boolean,
162-
): string {
236+
): FilterClause {
163237
if (!filters) {
164-
return '';
238+
return {} as FilterClause;
165239
}
166240

167241
if (isNestedFilter(filters)) {
@@ -255,7 +329,7 @@ function formatValue(
255329
type: ProcessType,
256330
): string {
257331
if (!fieldDef) {
258-
return `"${fieldValue}"`;
332+
return `${fieldValue}`;
259333
}
260334

261335
if (!isFieldFilterSupported) {
@@ -270,7 +344,7 @@ function formatValue(
270344
fieldDef.type.name === TypeName.Id ||
271345
fieldDef.type.name === TypeName.Date
272346
) {
273-
return `"${fieldValue}"`;
347+
return `${fieldValue}`;
274348
}
275349
throw new Error(
276350
`Failed to format value for ${fieldName} ${fieldValue} with type ${fieldDef.type.name}`,
@@ -301,3 +375,9 @@ function getGraphQLOperator(operator: FieldFilterOperatorEnum): string {
301375
throw new Error(`Operation "${operator}" not supported`);
302376
}
303377
}
378+
379+
// Function for getting 4 random digits to append to the clause variable name.
380+
// Not used for any secrets or anything
381+
function nonSecureRandomAlphaNumeric() {
382+
return randomBytes(8).toString('hex');
383+
}

0 commit comments

Comments
 (0)