Skip to content

Commit

Permalink
Merge pull request #247 from AikidoSec/patch-default-vars
Browse files Browse the repository at this point in the history
Use visit function to collect all StringValue's
  • Loading branch information
willem-delbare authored Jun 17, 2024
2 parents 2b8e6e0 + d43526f commit 8230161
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 87 deletions.
23 changes: 23 additions & 0 deletions library/sources/graphql/extractInputsFromDocument.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,26 @@ t.test("it handles nested query with argument", (t) => {
t.same(inputs, ["NestedCity"]);
t.end();
});

t.test("it parses default values for string arguments", (t) => {
const source = {
query: `query($id: ID = "default") {
user(id: $id) {
id
name
}
}`,
};

const document = parse(source.query);
const validationErrors = validate(schema, document);
if (validationErrors.length > 0) {
t.fail(validationErrors[0].message);
return;
}

const inputs = extractInputsFromDocument(document);

t.same(inputs, ["default"]);
t.end();
});
102 changes: 15 additions & 87 deletions library/sources/graphql/extractInputsFromDocument.ts
Original file line number Diff line number Diff line change
@@ -1,97 +1,25 @@
import type {
DocumentNode,
ValueNode,
SelectionSetNode,
FieldNode,
ArgumentNode,
FragmentDefinitionNode,
} from "graphql";

/**
* Extract user inputs from a value node
* @param value A value node
* @param inputs An array that will be filled with the user inputs
*/
function extractInputsFromValue(value: ValueNode, inputs: string[]) {
switch (value.kind) {
case "StringValue":
inputs.push(value.value);
break;
case "ListValue":
for (const item of value.values) {
extractInputsFromValue(item, inputs);
}
break;
case "ObjectValue":
for (const field of value.fields) {
extractInputsFromValue(field.value, inputs);
}
break;
default:
break;
}
}

/**
* Extract user inputs from a list of arguments
* @param args A array of argument nodes
* @param inputs An array that will be filled with the user inputs
*/
function extractInputsFromArguments(
args: ReadonlyArray<ArgumentNode>,
inputs: string[]
) {
for (const argument of args) {
extractInputsFromValue(argument.value, inputs);
}
}

/**
* A recursive function that traverses a selection set and extracts user inputs.
* @param selectionSet A graphql selection set node
* @param inputs An array that will be filled with the user inputs
*/
function traverseSelectionSet(
selectionSet: SelectionSetNode,
inputs: string[]
) {
for (const selection of selectionSet.selections) {
switch (selection.kind) {
case "Field":
const field = selection as FieldNode;
if (field.arguments) {
extractInputsFromArguments(field.arguments, inputs);
}
if (field.selectionSet) {
traverseSelectionSet(field.selectionSet, inputs);
}
break;
case "InlineFragment":
traverseSelectionSet(selection.selectionSet, inputs);
break;
default:
break;
}
}
}
import type { DocumentNode, StringValueNode } from "graphql";

/**
* This function extracts user inputs (that could be harmful) from a GraphQL document.
* @returns An array of user inputs.
*/
export function extractInputsFromDocument(document: DocumentNode): string[] {
let graphql;
try {
// Assuming graphql is installed when this function is called
// Don't use normal import for graphql
graphql = require("graphql");
} catch (e) {
return [];
}

const inputs: string[] = [];
graphql.visit(document, {
StringValue(node: StringValueNode) {
inputs.push(node.value);
},
});

for (const definition of document.definitions) {
switch (definition.kind) {
case "OperationDefinition":
traverseSelectionSet(definition.selectionSet, inputs);
break;
case "FragmentDefinition":
const fragment = definition as FragmentDefinitionNode;
traverseSelectionSet(fragment.selectionSet, inputs);
break;
}
}
return inputs;
}

0 comments on commit 8230161

Please sign in to comment.