{"id":15065800,"url":"https://github.com/devkral/graphene-protector","last_synced_at":"2025-04-10T13:34:15.954Z","repository":{"id":50771847,"uuid":"307862243","full_name":"devkral/graphene-protector","owner":"devkral","description":"graphene, strawberry and plain graphql protection against malicious requests","archived":false,"fork":false,"pushed_at":"2024-03-21T18:21:31.000Z","size":198,"stargazers_count":20,"open_issues_count":4,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-24T12:13:18.288Z","etag":null,"topics":["django","graphene","graphql","strawberry-graphql"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/devkral.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2020-10-28T00:23:37.000Z","updated_at":"2024-11-19T12:29:40.000Z","dependencies_parsed_at":"2024-03-21T19:37:12.222Z","dependency_job_id":null,"html_url":"https://github.com/devkral/graphene-protector","commit_stats":{"total_commits":77,"total_committers":1,"mean_commits":77.0,"dds":0.0,"last_synced_commit":"73fae0ae362b0a928a16714374a8481742c8a7d1"},"previous_names":[],"tags_count":32,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devkral%2Fgraphene-protector","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devkral%2Fgraphene-protector/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devkral%2Fgraphene-protector/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devkral%2Fgraphene-protector/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/devkral","download_url":"https://codeload.github.com/devkral/graphene-protector/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248102385,"owners_count":21048173,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["django","graphene","graphql","strawberry-graphql"],"created_at":"2024-09-25T00:54:05.920Z","updated_at":"2025-04-10T13:34:15.920Z","avatar_url":"https://github.com/devkral.png","language":"Python","readme":"# What does this project solve?\n\nIt provides protection against malicious grapqhl requests (resource exhaustion).\nDespite its name it can be used with graphql (pure), graphene, strawberry.\nIt is implemented via a custom ValidationRule,\nsupports error reporting and early bail out strategies as well as limits for single fields\n\n# Installation\n\n```sh\npip install graphene-protector\n```\n\n# Integration\n\n## Django\n\nThis adds to django the following setting:\n\n-   GRAPHENE_PROTECTOR_DEPTH_LIMIT: max depth\n-   GRAPHENE_PROTECTOR_SELECTIONS_LIMIT: max selections\n-   GRAPHENE_PROTECTOR_COMPLEXITY_LIMIT: max (depth \\* selections)\n-   GRAPHENE_PROTECTOR_PATH_INGORE_PATTERN: ignore fields in calculation (but still traverse them)\n\nIntegrate with:\n\ngraphene:\n\n```python 3\n# schema.py\n# replace normal Schema import with:\nfrom graphene_protector.django.graphene import Schema\nschema = Schema(query=Query, mutation=Mutation)\n```\n\nand add in django settings to GRAPHENE\n\n```python 3\n\nGRAPHENE = {\n    ...\n    \"SCHEMA\": \"path.to.schema\",\n}\n```\n\nor strawberry:\n\n```python 3\n# schema.py\n# replace normal Schema import with:\nfrom graphene_protector.django.strawberry import Schema\nschema = Schema(query=Query, mutation=Mutation)\n```\n\nmanual way (note: import from django for including defaults from settings)\n\n```python 3\nfrom graphene_protector.django.graphene import Schema\n# or\n# from graphene_protector.django.strawberry import Schema\nschema = Schema(query=Query)\nresult = schema.execute(query_string)\n```\n\nmanual way with custom default Limits\n\n```python 3\nfrom graphene_protector import Limits\nfrom graphene_protector.django.graphene import Schema\n# or\n# from graphene_protector.django.strawberry import Schema\nschema = graphene.Schema(query=Query, limits=Limits(complexity=None))\nresult = schema.execute(\n    query_string\n)\n\n```\n\n## Graphene \u0026 Strawberry\n\nlimits keyword with Limits object is supported.\n\n```python 3\nfrom graphene_protector import Limits\nfrom graphene_protector.graphene import Schema\n# or\n# from graphene_protector.strawberry import Schema\nschema = Schema(query=Query, limits=Limits(depth=20, selections=None, complexity=100))\nresult = schema.execute(query_string)\n```\n\n## pure graphql\n\n```python 3\n\nfrom graphene_protector import LimitsValidationRule\nfrom graphql.type.schema import Schema\nschema = Schema(\n    query=Query,\n)\nquery_ast = parse(\"{ hello }\")\nself.assertFalse(validate(schema, query_ast, [LimitsValidationRule]))\n\n```\n\nor with custom defaults\n\n```python 3\n\nfrom graphene_protector import Limits, LimitsValidationRule\nfrom graphql.type.schema import Schema\n\nclass CustomLimitsValidationRule(LimitsValidationRule):\n    default_limits = Limits(depth=20, selections=None, complexity=100)\n\nschema = Schema(\n    query=Query,\n)\nquery_ast = parse(\"{ hello }\")\nself.assertFalse(validate(schema, query_ast, [LimitsValidationRule]))\n\n```\n\nstrawberry extension variant\n\n```python 3\nfrom graphene_protector import Limits\nfrom graphene_protector.strawberry import CustomGrapheneProtector\nfrom strawberry import Schema\nschema = Schema(query=Query, extensions=[CustomGrapheneProtector(Limits(depth=20, selections=None, complexity=100))])\nresult = schema.execute(query_string)\n```\n\nor with custom defaults via Mixin\n\n```python 3\n\nfrom graphene_protector import Limits, SchemaMixin, LimitsValidationRule\nfrom graphql.type.schema import Schema\n\nclass CustomSchema(SchemaMixin, Schema):\n    protector_default_limits = Limits(depth=20, selections=None, complexity=100)\n\nschema = CustomSchema(\n    query=Query,\n)\nquery_ast = parse(\"{ hello }\")\nself.assertFalse(validate(schema, query_ast, [LimitsValidationRule]))\n\n```\n\nstrawberry variant with mixin (uses protector_per_operation_validation in contrast to the official graphene-protector strawberry schema)\n\n```python 3\nfrom graphene_protector import Limits, SchemaMixin, default_path_ignore_pattern\nfrom strawberry import Schema\n\nclass CustomSchema(SchemaMixin, Schema):\n    protector_default_limits = Limits(depth=20, selections=None, complexity=100)\n    protector_path_ignore_pattern = default_path_ignore_pattern\n\nschema = CustomSchema(query=Query)\nresult = schema.execute(query_string)\n```\n\nNote: for the mixin method all variables are prefixed in schema with `protector_`. Internally the `get_protector_` methods are used and mapped on the validation context. The extracted functions can be customized via the `protector_decorate_graphql_schema` method.\n\n## Limits\n\nA Limits object has following attributes:\n\n-   depth: max depth (default: 20, None disables feature)\n-   selections: max selections (default: None, None disables feature)\n-   complexity: max (depth subtree \\* selections subtree) (default: 100, None disables feature)\n-   gas: accumulated gas costs (default: None, None disables feature)\n-   passthrough: field names specified here will be passed through regardless if specified (default: empty frozen set)\n\nthey overwrite django settings if specified.\n\n## decorating single fields\n\nSometimes single fields should have different limits:\n\n```python\nfrom graphene_protector import Limits\nperson1 = Limits(depth=10)(graphene.Field(Person))\n```\n\nLimits are passthroughs for missing parameters\n\nThere is also a novel technique named gas: you can assign a field a static value or dynamically calculate it for the field\n\nThe decorator is called gas_usage\n\n```python\nfrom graphene_protector import gas_usage\nperson1 = gas_usage(10)(graphene.Field(Person))\n# dynamic way:\nperson2 = gas_usage(lambda **kwargs: 10)(graphene.Field(Person))\n\n```\n\nsee tests for more examples\n\n## one-time disable limit checks\n\nto disable checks for one operation use check_limits=False (works for:\nexecute, execute_async (if available), subscribe (if available)):\n\n```python 3\nfrom graphene_protector import Limits\nfrom graphene_protector.graphene import Schema\nschema = Schema(query=Query, limits=Limits(depth=20, selections=None, complexity=100))\nresult = schema.execute(query_string, check_limits=False)\n```\n\nUsefull for debugging or working around errors\n\n# Path ignoring\n\nThis is a feature for ignoring some path parts in calculation but still traversing them.\nIt is useful for e.g. relay which inflates the depth significant and can cause problems with complexity\nCurrently it is set to `edges/node$` which reduces the depth of connections by one.\nIf you want to ignore all children on a path then remove $ but be warned: it can be a long path and it is still traversed.\nThe path the regex matches agains is composed like this: `fieldname/subfields/...`.\n\nOther examples are:\n\n-   `node$|id$` for ignoring id fields in selection/complexity count and reducing the depth by 1 when seeing a node field\n-   `page_info|pageInfo` for ignoring page info in calculation (Note: you need only one, in case auto_snakecase=True only `pageInfo`)\n\nNote: items prefixed with `__` (internal names) are always ignored and not traversed.\n\nNote: if auto_snakecase is True, the path components are by default camel cased (overwritable via explicit `camelcase_path`)\n\nNote: gas is excluded from path ignoring\n\n# Gas\n\nGas should be a positive integer. Negative integers are possible but\nthe evaulation is stopped whenever the counter is above the limit so this is not reliable\n\nThe gas usage is annotated with the gas_usage decorator. A function can be passed\nwhich receives the following keyword arguments:\n\n-   schema_field\n-   fieldname\n-   parent (parent of schema_field)\n-   graphql_path\n\n# full validation\n\nOn the validation rule the validation is stopped by default when an error is found\nThis default can be overwritten and it is modified for the django code pathes.\nWhenever DEBUG is active a full validation happens, otherwise the shortcut is used.\nSee the source-code how to change your schema to have a custom hook for deciding if a full validation is done.\nIn addition the `path_ignore_pattern` and `limits` attributes can be also changed dynamically.\n\n# hooks\n\nThe validation rule uses some `protector_` prefixed methods from the schema.\nWith this you can customize the default behaviour.\nIt is used by the django mixin to read the settings (see django) and to react on DEBUG with full_validation\n\n# Development\n\nI am open for new ideas.\nIf you want some new or better algorithms integrated just make a PR\n\n## Internals\n\nPath ignoring is ignored for the gas calculation (gas is always explicit). Therefor there is no way to stop when an open path was found (all children are ignored).\n\nThis project uses a \"stack free\" recursive approach. Instead of calling recursively, generators are used to remember the position and to continue.\n\nNote: graphql itself will fail because they are not using a stack free approach. For graphql there was a limit around 200 depth. The graphql tree cannot be constructed so there is no way to evaluate this.\n\n# related projects:\n\n-   secure-graphene: lacks django integration, some features and has a not so easy findable name.\n    But I accept: it is the \"not invented here\"-syndrome\n-   cost specs: https://ibm.github.io/graphql-specs/cost-spec.html\n    looks nice but very hard to implement. Handling costs at client and server side synchronously is complicated.\n    Also the costs are baked into the schema, which crosses the boundary between static and dynamic\n\n# Security Advise\n\nPlease note, that this project doesn't prevent resource exhaustion attacks by using a huge amount of tokens. This project\nprevents attacks after the string has been parsed to a node graph.\n\nPlease see token limiter (e.g. strawberry.extensions TokenLimiter) for that purpose. Or set manually the token limit to an appropiate value\ne.g. 1000 (ExecutionContext), see the strawbbery extension for an example\n\nNote also, that because of the recursive parsing of strings, there is the possibility to cause an exception\nby using very deep graphs (\u003e 200 level).\nBecause this attack is also taking place while string parsing (string to graph), I cannot stop it.\nThe effects are limited because of the security features of python (stops after 1000 level depth) and returns an exception which stops the graph parsing\n\n# TODO\n\n-   manually construct the graphql tree for tests for check_resource_usage\n-   fill RessourceLimits (graphql errors) with details like the field where the limit was reached\n-   improve documentation\n-   keep an eye on the performance impact of the new path regex checking\n-   add tests for auto_snakecase and camelcase_path\n-   skip tests in case settings are not matching\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevkral%2Fgraphene-protector","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdevkral%2Fgraphene-protector","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevkral%2Fgraphene-protector/lists"}