diff --git a/.env.githubci b/.env.githubci index 64b2301..8d90e6f 100644 --- a/.env.githubci +++ b/.env.githubci @@ -47,6 +47,7 @@ SCYLLA_REPLICATION_FACTOR='1' SCYLLA_COMPACTION_STRATEGY='SizeTieredCompactionStrategy' SCYLLA_HAS_ENTERPRISE_FEATURES='false' +GRAPHQL_MAX_DEPTH=10 EXPOSE_SENSITIVE_IMPLEMENTATION_DETAILS_IN_ERRORS=true ALLOW_USER_INPUT_LOCALHOST_URIS=true SEQUELIZE_PRINT_LOGS=true diff --git a/server/.env.example b/server/.env.example index 6313f56..ff55279 100644 --- a/server/.env.example +++ b/server/.env.example @@ -81,3 +81,4 @@ GROQ_SECRET_KEY= ITEM_QUEUE_TRAFFIC_PERCENTAGE='0.05' UI_URL=http://localhost:3000 +GRAPHQL_MAX_DEPTH=10 diff --git a/server/api.ts b/server/api.ts index 27f5999..914726d 100644 --- a/server/api.ts +++ b/server/api.ts @@ -24,6 +24,7 @@ import cors from 'cors'; import express, { type ErrorRequestHandler } from 'express'; import session from 'express-session'; import { buildContext, GraphQLLocalStrategy } from 'graphql-passport'; +import depthLimit from 'graphql-depth-limit'; import helmet from 'helmet'; import passport from 'passport'; import { MultiSamlStrategy } from '@node-saml/passport-saml'; @@ -37,6 +38,7 @@ import resolvers from './graphql/resolvers.js'; import typeDefs from './graphql/schema.js'; import { authSchemaWrapper } from './graphql/utils/authorization.js'; import { type Dependencies } from './iocContainer/index.js'; +import { safeGetEnvInt } from './iocContainer/utils.js'; import controllers from './routes/index.js'; import { jsonStringify } from './utils/encoding.js'; import { @@ -373,6 +375,7 @@ export default async function makeApiServer(deps: Dependencies) { : ApolloServerPluginLandingPageGraphQLPlayground()), }, ], + validationRules: [depthLimit(safeGetEnvInt('GRAPHQL_MAX_DEPTH', 10))], introspection: process.env.NODE_ENV !== 'production', formatError(e) { // `e` can be an ApolloError instance, but will only be one if such an diff --git a/server/iocContainer/utils.ts b/server/iocContainer/utils.ts index 2d75446..c667c78 100644 --- a/server/iocContainer/utils.ts +++ b/server/iocContainer/utils.ts @@ -3,6 +3,7 @@ import type Bottle from '@ethanresnick/bottlejs'; import { __throw } from '../utils/misc.js'; +import { jsonStringify } from '../utils/encoding.js'; import { type Dependencies as Deps } from './index.js'; const DEPENDENCIES = Symbol(); @@ -661,3 +662,21 @@ export function safeGetEnvVar(varName: string): string { process.env[varName] ?? __throw(new Error(`Missing env var ${varName}`)) ); } + +/** + * Gets an env var and parses it as a positive integer. Returns `defaultValue` + * if the variable is unset or invalid, logging an error on misconfiguration. + */ +export function safeGetEnvInt(varName: string, defaultValue: number): number { + const raw = process.env[varName]; + if (raw === undefined) return defaultValue; + const parsed = parseInt(raw, 10); + if (!Number.isInteger(parsed) || parsed <= 0) { + // eslint-disable-next-line no-console + console.error( + `Invalid env var ${varName}: expected a positive integer, got ${jsonStringify(raw)}. Using default value ${defaultValue}.`, + ); + return defaultValue; + } + return parsed; +} diff --git a/server/package-lock.json b/server/package-lock.json index 611ee9d..6238dcb 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -65,6 +65,7 @@ "fuzzball": "^2.1.2", "generic-pool": "^3.8.2", "graphql": "^16.0.1", + "graphql-depth-limit": "^1.1.0", "graphql-passport": "^0.6.4", "graphql-scalars": "^1.19.0", "helmet": "^4.6.0", @@ -103,6 +104,7 @@ "devDependencies": { "@faker-js/faker": "^7.5.0", "@types/cls-hooked": "^4.3.3", + "@types/graphql-depth-limit": "^1.1.6", "@types/jest": "^29.2.4", "@types/js-yaml": "^4.0.5", "@types/stream-json": "^1.7.7", @@ -1474,6 +1476,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", @@ -10543,6 +10546,30 @@ "@types/node": "*" } }, + "node_modules/@types/graphql-depth-limit": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@types/graphql-depth-limit/-/graphql-depth-limit-1.1.6.tgz", + "integrity": "sha512-WU4bjoKOzJ8CQE32Pbyq+YshTMcLJf2aJuvVtSLv1BQPwDUGa38m2Vr8GGxf0GZ0luCQcfxlhZeHKu6nmTBvrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graphql": "^14.5.3" + } + }, + "node_modules/@types/graphql-depth-limit/node_modules/graphql": { + "version": "14.7.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.7.0.tgz", + "integrity": "sha512-l0xWZpoPKpppFzMfvVyFmp9vLN7w/ZZJPefUicMCepfJeQ8sMcztloGYY9DfjVPo6tIUDzU5Hw3MUbIjj9AVVA==", + "deprecated": "No longer supported; please update to a newer version. Details: https://github.com/graphql/graphql-js#version-support", + "dev": true, + "license": "MIT", + "dependencies": { + "iterall": "^1.2.2" + }, + "engines": { + "node": ">= 6.x" + } + }, "node_modules/@types/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", @@ -10643,7 +10670,8 @@ "node_modules/@types/node": { "version": "18.17.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.3.tgz", - "integrity": "sha512-2x8HWtFk0S99zqVQABU9wTpr8wPoaDHZUcAkoTKH+nL7kPv3WUI9cRi/Kk5Mz4xdqXSqTkKP7IWNoQQYCnDsTA==" + "integrity": "sha512-2x8HWtFk0S99zqVQABU9wTpr8wPoaDHZUcAkoTKH+nL7kPv3WUI9cRi/Kk5Mz4xdqXSqTkKP7IWNoQQYCnDsTA==", + "peer": true }, "node_modules/@types/node-fetch": { "version": "2.6.4", @@ -11023,6 +11051,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.20.0.tgz", "integrity": "sha512-bYerPDF/H5v6V76MdMYhjwmwgMA+jlPVqjSDq2cRqMi8bP5sR3Z+RLOiOMad3nsnmDVmn2gAFCyNgh/dIrfP/w==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.20.0", "@typescript-eslint/types": "6.20.0", @@ -11449,6 +11478,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -11507,6 +11537,7 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -11997,6 +12028,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -12225,6 +12257,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001517", "electron-to-chromium": "^1.4.477", @@ -13373,6 +13406,7 @@ "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -13583,6 +13617,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", "integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.6", "array.prototype.findlastindex": "^1.2.2", @@ -14193,6 +14228,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -15192,10 +15228,35 @@ "version": "16.8.2", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.2.tgz", "integrity": "sha512-cvVIBILwuoSyD54U4cF/UXDh5yAobhNV/tPygI4lZhgOIJQE/WLWC4waBRb4I6bDVYb3OVx3lfHbaQOEoUD5sg==", + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/graphql-depth-limit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/graphql-depth-limit/-/graphql-depth-limit-1.1.0.tgz", + "integrity": "sha512-+3B2BaG8qQ8E18kzk9yiSdAa75i/hnnOwgSeAxVJctGQPvmeiLtqKOYF6HETCyRjiF7Xfsyal0HbLlxCQkgkrw==", + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "graphql": "*" + } + }, + "node_modules/graphql-depth-limit/node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/graphql-passport": { "version": "0.6.7", "resolved": "https://registry.npmjs.org/graphql-passport/-/graphql-passport-0.6.7.tgz", @@ -15606,6 +15667,7 @@ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.2.tgz", "integrity": "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==", "license": "MIT", + "peer": true, "dependencies": { "@ioredis/commands": "1.5.0", "cluster-key-slot": "^1.1.0", @@ -15992,7 +16054,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==", - "optional": true + "devOptional": true }, "node_modules/jackspeak": { "version": "3.4.0", @@ -16016,6 +16078,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.2.tgz", "integrity": "sha512-8eQg2mqFbaP7CwfsTpCxQ+sHzw1WuNWL5UUvjnWP4hx2riGz9fPSzYOaU5q8/GqWn1TfgZIVTqYJygbGbWAANg==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.6.2", "@jest/types": "^29.6.1", @@ -17891,6 +17954,7 @@ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", "license": "MIT", + "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -19908,6 +19972,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -20017,6 +20082,7 @@ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", "dev": true, + "peer": true, "dependencies": { "tslib": "^1.8.1" }, @@ -20163,6 +20229,7 @@ "version": "5.5.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -20232,6 +20299,7 @@ "version": "7.14.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz", "integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.14.1", "@typescript-eslint/types": "7.14.1", diff --git a/server/package.json b/server/package.json index 73fb823..cf9932f 100644 --- a/server/package.json +++ b/server/package.json @@ -79,6 +79,7 @@ "fuzzball": "^2.1.2", "generic-pool": "^3.8.2", "graphql": "^16.0.1", + "graphql-depth-limit": "^1.1.0", "graphql-passport": "^0.6.4", "graphql-scalars": "^1.19.0", "helmet": "^4.6.0", @@ -117,6 +118,7 @@ "devDependencies": { "@faker-js/faker": "^7.5.0", "@types/cls-hooked": "^4.3.3", + "@types/graphql-depth-limit": "^1.1.6", "@types/jest": "^29.2.4", "@types/js-yaml": "^4.0.5", "@types/stream-json": "^1.7.7",