-
Notifications
You must be signed in to change notification settings - Fork 53
feat: add misc graphql validation rules #686
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
a5e5a08
feat: implement custom validation rules for duplicate fields and alia…
rohan-chaturvedi 89d6d7a
chore: misc cleanup and documentation, reduce limits
rohan-chaturvedi b3f1086
feat: add access control checks for environment in bulk secret mutat…
rohan-chaturvedi 6c9ecab
fix: add validation for role changes, invite roles
rohan-chaturvedi fe1636a
fix: client IP discovery utils (#687)
nimish-ks 4e222cf
fix: add env access check for bulk secret deletes
rohan-chaturvedi da65c38
fix: validate service account roles during create and update
rohan-chaturvedi b5fe1d3
fix: add safe fallback to raw ip in case header is missing
rohan-chaturvedi 8d9e2b2
Merge branch 'main' into fix--graphql-server-hardening
rohan-chaturvedi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| from ipaddress import ip_address | ||
|
|
||
|
|
||
| def get_client_ip(request): | ||
| """ | ||
| Get the client IP address as a single string. | ||
|
|
||
| Args: | ||
| request: Django request object | ||
|
|
||
| Returns: | ||
| str | None: The client IP address (IPv4 or IPv6) | ||
| """ | ||
| raw_ip = (request.META.get("HTTP_X_REAL_IP") or "").strip() | ||
| if not raw_ip: | ||
| return None | ||
| try: | ||
| ip_address(raw_ip) | ||
| except ValueError: | ||
| return None | ||
| return raw_ip |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,15 @@ | ||
| from graphene_django.views import GraphQLView | ||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||
| from graphql import specified_rules | ||
| from backend.graphene.validation import DuplicateFieldLimitRule, AliasUsageLimitRule | ||
|
|
||
|
|
||
| CUSTOM_RULES = tuple(specified_rules) + ( | ||
| DuplicateFieldLimitRule, | ||
| AliasUsageLimitRule, | ||
| ) | ||
|
|
||
|
|
||
| class PrivateGraphQLView(LoginRequiredMixin, GraphQLView): | ||
| raise_exception = True | ||
| pass | ||
| validation_rules = CUSTOM_RULES |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| from collections import Counter | ||
| from graphql import GraphQLError | ||
| from graphql.language.ast import FieldNode | ||
| from graphql.validation import ValidationRule | ||
|
|
||
|
|
||
| class DuplicateFieldLimitRule(ValidationRule): | ||
rohan-chaturvedi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """Limits how many times the same response field (name or alias) can appear | ||
| in a single selection set. Uses a Counter stack to track counts per nested | ||
| selection set and reports an error if any exceeds MAX_DUPLICATE_FIELDS.""" | ||
|
|
||
| MAX_DUPLICATE_FIELDS = 20 | ||
rohan-chaturvedi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def __init__(self, context): | ||
| super().__init__(context) | ||
| self.max_duplicates = self.MAX_DUPLICATE_FIELDS | ||
| self._stack = [] # Stack of Counters for nested selection sets | ||
|
|
||
| def enter_selection_set(self, *_): | ||
| """Push a new Counter for a nested selection set.""" | ||
| self._stack.append(Counter()) | ||
|
|
||
| def leave_selection_set(self, node, *_): | ||
| """Pop Counter, emit an error for any response name exceeding limit.""" | ||
| counts = self._stack.pop() | ||
| for response_name, hits in counts.items(): | ||
| if hits > self.max_duplicates: | ||
| offending = [ | ||
| selection | ||
| for selection in node.selections | ||
| if isinstance(selection, FieldNode) | ||
| and ( | ||
| selection.alias.value | ||
| if selection.alias | ||
| else selection.name.value | ||
| ) | ||
| == response_name | ||
| ] | ||
| self.context.report_error( | ||
| GraphQLError( | ||
| f"Field '{response_name}' requested {hits} times; limit is {self.max_duplicates}.", | ||
| nodes=offending or [node], | ||
| ) | ||
| ) | ||
|
|
||
| def enter_field(self, node, *_): | ||
| """Increment count for this field’s response name (alias or original).""" | ||
| response_name = node.alias.value if node.alias else node.name.value | ||
| if not self._stack: | ||
| self._stack.append(Counter()) | ||
| self._stack[-1][response_name] += 1 | ||
|
|
||
|
|
||
| class AliasUsageLimitRule(ValidationRule): | ||
rohan-chaturvedi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """Caps the number of aliases used within a single operation definition. | ||
| Tracks alias count per operation; reports an error when exceeding | ||
| MAX_ALIAS_FIELDS.""" | ||
|
|
||
rohan-chaturvedi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| MAX_ALIAS_FIELDS = 20 | ||
|
|
||
| def __init__(self, context): | ||
| super().__init__(context) | ||
| self.max_aliases = self.MAX_ALIAS_FIELDS | ||
| self._operation_alias_counts = ( | ||
| [] | ||
| ) # Stack for nested operations (fragments not counted) | ||
rohan-chaturvedi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def enter_operation_definition(self, *_): | ||
| """Start alias count for a new operation.""" | ||
| self._operation_alias_counts.append(0) | ||
|
|
||
| def leave_operation_definition(self, *_): | ||
| """End alias count scope for the operation.""" | ||
| self._operation_alias_counts.pop() | ||
|
|
||
| def enter_field(self, node, *_): | ||
| """Increment alias counter when a field has an alias; error if limit exceeded.""" | ||
| if node.alias: | ||
| if not self._operation_alias_counts: | ||
| self._operation_alias_counts.append(0) | ||
rohan-chaturvedi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| alias_count = self._operation_alias_counts[-1] + 1 | ||
| self._operation_alias_counts[-1] = alias_count | ||
| if alias_count > self.max_aliases: | ||
| self.context.report_error( | ||
| GraphQLError( | ||
| f"Alias limit of {self.max_aliases} exceeded in a single operation.", | ||
| nodes=[node], | ||
| ) | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.