@aikidosec/firewall
Version:
Zen by Aikido is an embedded Application Firewall that autonomously protects Node.js apps against common and critical attacks, provides rate limiting, detects malicious traffic (including bots), and more.
168 lines (167 loc) • 7.27 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.GraphQL = void 0;
const Context_1 = require("../agent/Context");
const isPlainObject_1 = require("../helpers/isPlainObject");
const extractInputsFromDocument_1 = require("./graphql/extractInputsFromDocument");
const extractTopLevelFieldsFromDocument_1 = require("./graphql/extractTopLevelFieldsFromDocument");
const isGraphQLOverHTTP_1 = require("./graphql/isGraphQLOverHTTP");
const shouldRateLimitOperation_1 = require("./graphql/shouldRateLimitOperation");
const wrapExport_1 = require("../agent/hooks/wrapExport");
class GraphQL {
discoverGraphQLSchema(context, executeArgs, agent) {
if (!this.printSchema) {
return;
}
if (!executeArgs.schema) {
return;
}
if (!context.method || !context.route) {
return;
}
if (!agent.hasGraphQLSchema(context.method, context.route)) {
try {
const schema = this.printSchema(executeArgs.schema);
agent.onGraphQLSchema(context.method, context.route, schema);
}
catch {
// Ignore errors
}
}
}
discoverGraphQLQueryFields(context, executeArgs, agent) {
if (!context.method || !context.route) {
return;
}
const topLevelFields = (0, extractTopLevelFieldsFromDocument_1.extractTopLevelFieldsFromDocument)(executeArgs.document, executeArgs.operationName ? executeArgs.operationName : undefined);
if (topLevelFields) {
agent.onGraphQLExecute(context.method, context.route, topLevelFields.type, topLevelFields.fields.map((field) => field.name.value));
}
}
inspectGraphQLExecute(args, agent) {
if (!Array.isArray(args) || typeof args[0] !== "object" || !this.visit) {
return;
}
const executeArgs = args[0];
const context = (0, Context_1.getContext)();
if (!context) {
// We expect the context to be set by the wrapped http server
return;
}
if (context &&
context.method &&
context.route &&
(0, isGraphQLOverHTTP_1.isGraphQLOverHTTP)(context)) {
// We only want to discover GraphQL over HTTP
// We should ignore queries coming from a GraphQL client in SSR mode
this.discoverGraphQLSchema(context, executeArgs, agent);
this.discoverGraphQLQueryFields(context, executeArgs, agent);
}
const userInputs = (0, extractInputsFromDocument_1.extractInputsFromDocument)(executeArgs.document, this.visit);
if (executeArgs.variableValues &&
typeof executeArgs.variableValues === "object") {
for (const value of Object.values(executeArgs.variableValues)) {
if (typeof value === "string") {
userInputs.push(value);
}
}
}
if (Array.isArray(context.graphql)) {
(0, Context_1.updateContext)(context, "graphql", context.graphql.concat(userInputs));
}
else {
(0, Context_1.updateContext)(context, "graphql", userInputs);
}
}
handleRateLimiting(args, origReturnVal, agent) {
const context = (0, Context_1.getContext)();
if (!context || !agent || !this.GraphQLError) {
return origReturnVal;
}
if (!Array.isArray(args) || !(0, isPlainObject_1.isPlainObject)(args[0])) {
return origReturnVal;
}
const result = (0, shouldRateLimitOperation_1.shouldRateLimitOperation)(agent, context, args[0]);
if (result.block) {
// Mark the request as rate limited in the context
(0, Context_1.updateContext)(context, "rateLimitedEndpoint", result.endpoint);
return {
errors: [
new this.GraphQLError("You are rate limited by Zen.", {
nodes: [result.field],
extensions: {
code: "RATE_LIMITED_BY_ZEN",
ipAddress: context.remoteAddress,
},
}),
],
};
}
return origReturnVal;
}
wrapExecution(exports, pkgInfo) {
const methods = ["execute", "executeSync"];
for (const method of methods) {
(0, wrapExport_1.wrapExport)(exports, method, pkgInfo, {
kind: "graphql_op",
modifyReturnValue: (args, returnValue, agent) => this.handleRateLimiting(args, returnValue, agent),
inspectArgs: (args, agent) => this.inspectGraphQLExecute(args, agent),
});
}
}
wrap(hooks) {
const graphqlPkg = hooks
.addPackage("graphql")
.withVersion("^15.0.0 || ^16.0.0");
graphqlPkg
.onFileRequire("execution/execute.js", (exports, pkgInfo) => {
this.wrapExecution(exports, pkgInfo);
})
.onRequire((exports) => {
this.printSchema = exports.printSchema;
this.visit = exports.visit;
this.GraphQLError = exports.GraphQLError;
})
.addMultiFileInstrumentation(["execution/execute.js", "execution/execute.mjs"], ["execute", "executeSync"].map((method) => ({
name: method,
operationKind: "graphql_op",
nodeType: "FunctionDeclaration",
modifyReturnValue: (args, returnValue, agent) => this.handleRateLimiting(args, returnValue, agent),
inspectArgs: (args, agent) => this.inspectGraphQLExecute(args, agent),
})));
const localVariableAndFiles = new Map([
["language/visitor.js", "visit"],
["language/visitor.mjs", "visit"],
["utilities/printSchema.js", "printSchema"],
["utilities/printSchema.mjs", "printSchema"],
["error/GraphQLError.js", "GraphQLError"],
["error/GraphQLError.mjs", "GraphQLError"],
]);
for (const [file, variable] of localVariableAndFiles) {
graphqlPkg.addFileInstrumentation({
path: file,
functions: [],
accessLocalVariables: {
names: [variable],
cb: (value) => {
this[variable] = value[0];
},
},
});
}
hooks
.addPackage("@graphql-tools/executor")
.withVersion("^1.0.0")
.onFileRequire("cjs/execution/execute.js", (exports, pkgInfo) => {
this.wrapExecution(exports, pkgInfo);
})
.addMultiFileInstrumentation(["cjs/execution/execute.js", "esm/execution/execute.js"], ["execute", "executeSync"].map((method) => ({
name: method,
operationKind: "graphql_op",
nodeType: "FunctionDeclaration",
modifyReturnValue: (args, returnValue, agent) => this.handleRateLimiting(args, returnValue, agent),
inspectArgs: (args, agent) => this.inspectGraphQLExecute(args, agent),
})));
}
}
exports.GraphQL = GraphQL;