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
79 changes: 79 additions & 0 deletions packages/web/app/src/lib/urql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,85 @@ export const urqlClient = createClient({
ProjectTargetsResourceAssignment: noKey,
ProjectResourceAssignment: noKey,
BillingConfiguration: noKey,
SchemaChangeMeta: noKey,
SchemaCheckMeta: noKey,
FieldArgumentDescriptionChanged: noKey,
FieldArgumentTypeChanged: noKey,
DirectiveRemoved: noKey,
DirectiveAdded: noKey,
DirectiveDescriptionChanged: noKey,
DirectiveLocationAdded: noKey,
DirectiveLocationRemoved: noKey,
DirectiveArgumentAdded: noKey,
DirectiveArgumentRemoved: noKey,
DirectiveArgumentDescriptionChanged: noKey,
DirectiveArgumentDefaultValueChanged: noKey,
DirectiveArgumentTypeChanged: noKey,
EnumValueRemoved: noKey,
EnumValueAdded: noKey,
EnumValueDescriptionChanged: noKey,
EnumValueDeprecationReasonChanged: noKey,
EnumValueDeprecationReasonAdded: noKey,
EnumValueDeprecationReasonRemoved: noKey,
FieldRemoved: noKey,
FieldAdded: noKey,
FieldDescriptionChanged: noKey,
FieldDescriptionAdded: noKey,
FieldDescriptionRemoved: noKey,
FieldDeprecationAdded: noKey,
FieldDeprecationRemoved: noKey,
FieldDeprecationReasonChanged: noKey,
FieldDeprecationReasonAdded: noKey,
FieldDeprecationReasonRemoved: noKey,
FieldTypeChanged: noKey,
DirectiveUsageUnionMemberAdded: noKey,
DirectiveUsageUnionMemberRemoved: noKey,
FieldArgumentAdded: noKey,
FieldArgumentRemoved: noKey,
InputFieldRemoved: noKey,
InputFieldAdded: noKey,
InputFieldDescriptionAdded: noKey,
InputFieldDescriptionRemoved: noKey,
InputFieldDescriptionChanged: noKey,
InputFieldDefaultValueChanged: noKey,
InputFieldTypeChanged: noKey,
ObjectTypeInterfaceAdded: noKey,
ObjectTypeInterfaceRemoved: noKey,
SchemaQueryTypeChanged: noKey,
SchemaMutationTypeChanged: noKey,
SchemaSubscriptionTypeChanged: noKey,
TypeRemoved: noKey,
TypeAdded: noKey,
TypeKindChanged: noKey,
TypeDescriptionChanged: noKey,
TypeDescriptionAdded: noKey,
TypeDescriptionRemoved: noKey,
UnionMemberRemoved: noKey,
UnionMemberAdded: noKey,
DirectiveUsageEnumAdded: noKey,
DirectiveUsageEnumRemoved: noKey,
DirectiveUsageEnumValueAdded: noKey,
DirectiveUsageEnumValueRemoved: noKey,
DirectiveUsageInputObjectRemoved: noKey,
DirectiveUsageInputObjectAdded: noKey,
DirectiveUsageInputFieldDefinitionAdded: noKey,
DirectiveUsageInputFieldDefinitionRemoved: noKey,
DirectiveUsageFieldAdded: noKey,
DirectiveUsageFieldRemoved: noKey,
DirectiveUsageScalarAdded: noKey,
DirectiveUsageScalarRemoved: noKey,
DirectiveUsageObjectAdded: noKey,
DirectiveUsageObjectRemoved: noKey,
DirectiveUsageInterfaceAdded: noKey,
DirectiveUsageSchemaAdded: noKey,
DirectiveUsageSchemaRemoved: noKey,
DirectiveUsageFieldDefinitionAdded: noKey,
DirectiveUsageFieldDefinitionRemoved: noKey,
DirectiveUsageArgumentDefinitionRemoved: noKey,
DirectiveUsageInterfaceRemoved: noKey,
DirectiveUsageArgumentDefinitionAdded: noKey,
DirectiveUsageArgumentAdded: noKey,
DirectiveUsageArgumentRemoved: noKey,
},
globalIDs: ['SuccessfulSchemaCheck', 'FailedSchemaCheck'],
}),
Expand Down
1 change: 1 addition & 0 deletions packages/web/app/src/pages/target-proposal-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type ServiceProposalDetails = {
compositionErrors?: FragmentType<typeof CompositionErrorsSection_SchemaErrorConnection>;
beforeSchema: GraphQLSchema | null;
afterSchema: GraphQLSchema | null;
buildError: Error | null;
allChanges: Change<any>[];
// Required because the component ChangesBlock uses this fragment.
rawChanges: FragmentType<typeof ProposalOverview_ChangeFragment>[];
Expand Down
43 changes: 31 additions & 12 deletions packages/web/app/src/pages/target-proposal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { buildSchema } from 'graphql';
import { buildSchema, GraphQLSchema } from 'graphql';
import { useMutation, useQuery, UseQueryExecute } from 'urql';
import { Page, TargetLayout } from '@/components/layouts/target';
import { CompositionErrorsSection_SchemaErrorConnection } from '@/components/target/history/errors-and-changes';
Expand Down Expand Up @@ -224,6 +224,9 @@ const ProposalsContent = (props: Parameters<typeof TargetProposalsSinglePage>[0]
// Takes all the data provided by the queries to apply the patch to the schema and
// categorize changes.
const services = useMemo(() => {
if (changesQuery.fetching || query.fetching) {
return [];
}
return (
changesQuery.data?.schemaProposal?.checks?.edges?.map(
({ node: proposalVersion }): ServiceProposalDetails => {
Expand All @@ -241,6 +244,7 @@ const ProposalsContent = (props: Parameters<typeof TargetProposalsSinglePage>[0]
latestSchema.__typename === 'SingleSchema' /* &&
(proposalVersion.serviceName == null || proposalVersion.serviceName === '') */,
)?.node.source;

const beforeSchema = existingSchema?.length
? buildSchema(existingSchema, { assumeValid: true, assumeValidSDL: true })
: null;
Expand All @@ -266,21 +270,34 @@ const ProposalsContent = (props: Parameters<typeof TargetProposalsSinglePage>[0]
}) ?? [];
const conflictingChanges: Array<{ change: Change; error: Error }> = [];
const ignoredChanges: Array<{ change: Change; error: Error }> = [];
const afterSchema = beforeSchema
? patchSchema(beforeSchema, allChanges, {
onError(error, change) {
if (error instanceof NoopError) {
ignoredChanges.push({ change, error });
}
conflictingChanges.push({ change, error });
return errors.looseErrorHandler(error, change);
},
})
: buildSchema(proposalVersion.schemaSDL, { assumeValid: true, assumeValidSDL: true });
let buildError: Error | null = null;
let afterSchema: GraphQLSchema | null = null;
if (beforeSchema) {
afterSchema = patchSchema(beforeSchema, allChanges, {
onError(error, change) {
if (error instanceof NoopError) {
ignoredChanges.push({ change, error });
}
conflictingChanges.push({ change, error });
return errors.looseErrorHandler(error, change);
},
});
} else {
try {
afterSchema = buildSchema(proposalVersion.schemaSDL, {
assumeValid: true,
assumeValidSDL: true,
});
} catch (e: unknown) {
console.error(e);
buildError = e as Error;
}
}
Comment on lines +275 to +295
Copy link
Contributor

Choose a reason for hiding this comment

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

security-medium medium

The buildSchema and patchSchema functions, which are called with user-provided schema data, are vulnerable to a client-side Denial of Service (DoS) attack. Maliciously crafted schema data (e.g., deep recursion or large changes) can cause excessive CPU/memory consumption, blocking the main thread and crashing the browser tab. Although a try-catch block is present for buildSchema, it only prevents an uncaught error from crashing the page, not the DoS itself. To mitigate this, it is strongly recommended to run buildSchema and patchSchema inside a Web Worker to offload these potentially long-running tasks from the main thread. Additionally, within the error handling, consider making the type assertion for e safer by checking if e is an instance of Error before accessing its properties, as e as Error can lead to unexpected behavior if a non-Error value is thrown.


return {
beforeSchema,
afterSchema,
buildError,
allChanges,
rawChanges: proposalVersion.schemaChanges?.edges.map(({ node }) => node) ?? [],
conflictingChanges,
Expand All @@ -295,6 +312,8 @@ const ProposalsContent = (props: Parameters<typeof TargetProposalsSinglePage>[0]
// @todo handle pagination
changesQuery.data?.schemaProposal?.checks?.edges,
query.data?.latestValidVersion?.schemas.edges,
changesQuery.fetching,
query.fetching,
]);
const proposal = query.data?.schemaProposal;

Expand Down
Loading