Skip to content
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

Add strawberry.cast #3749

Merged
merged 1 commit into from
Jan 9, 2025
Merged

Add strawberry.cast #3749

merged 1 commit into from
Jan 9, 2025

Conversation

bellini666
Copy link
Member

@bellini666 bellini666 commented Jan 9, 2025

The common node: Node used to resolve relay nodes means we will be relying on is_type_of to check if the returned object is in fact a subclass of the Node interface.

However, integrations such as Django, SQLAlchemy and Pydantic will not return the type itself, but instead an alike object that is later resolved to the expected type.

In case there are more than one possible type defined for that model that is being returned, the first one that replies True to is_type_of check would be used in the resolution, meaning that when asking for PublicUser:123, strawberry could end up returning User:123, which can lead to security issues (such as data leakage).

In here we are introducing a new strawberry.cast, which will be used to mark an object with the already known type by us, and when asking for is_type_of that mark will be used to check instead, ensuring we will return the correct type.

Summary by Sourcery

Bug Fixes:

  • Fix a security issue where resolving a relay node with multiple possibilities could return the wrong type, leading to potential data leakage.

@bellini666 bellini666 requested a review from patrick91 January 9, 2025 17:49
@bellini666 bellini666 self-assigned this Jan 9, 2025
Copy link
Contributor

sourcery-ai bot commented Jan 9, 2025

Reviewer's Guide by Sourcery

This pull request introduces a new strawberry.cast function to resolve a security issue when resolving relay nodes with multiple possibilities. The issue arises when integrations like Django, SQLAlchemy, and Pydantic return an object that is later resolved to the expected type. If there's more than one possible type, the first one that passes the is_type_of check is used, potentially leading to data leakage. The strawberry.cast function marks an object with its known type, ensuring the correct type is returned during resolution.

Sequence diagram for the improved relay node resolution

sequenceDiagram
    participant Client
    participant GraphQL
    participant RelayResolver
    participant TypeResolver

    Client->>GraphQL: Query node(id: 'PublicUser:123')
    GraphQL->>RelayResolver: Resolve node
    RelayResolver->>TypeResolver: resolve_type()
    TypeResolver-->>RelayResolver: node_type
    RelayResolver->>RelayResolver: resolve_node()
    Note over RelayResolver: New: Apply strawberry.cast
    RelayResolver->>TypeResolver: is_type_of check
    Note over TypeResolver: Check __as_strawberry_type__
    TypeResolver-->>RelayResolver: Correct type confirmed
    RelayResolver-->>GraphQL: Correctly typed node
    GraphQL-->>Client: Response
Loading

Class diagram for the new cast functionality

classDiagram
    class StrawberryCastObj {
        <<Protocol>>
        +__as_strawberry_type__: ClassVar[type]
    }

    class cast {
        +cast(type_: type, obj: object) StrawberryCastObj
        +cast(type_: type, obj: None) None
    }

    class Node {
        <<interface>>
        +resolve_node()
        +is_type_of()
    }

    StrawberryCastObj <|-- Node
    note for StrawberryCastObj "New protocol for type-cast objects"
    note for cast "New function to safely cast objects to their known type"
Loading

State diagram for node type resolution process

stateDiagram-v2
    [*] --> ReceiveNode
    ReceiveNode --> CheckCast: Check if object is cast
    CheckCast --> UseCastType: Has __as_strawberry_type__
    CheckCast --> StandardResolution: No cast type

    StandardResolution --> CheckObjectDefinition
    CheckObjectDefinition --> UseOriginType: Has object definition
    CheckObjectDefinition --> TypeCheck: No object definition

    UseCastType --> [*]: Return correct type
    UseOriginType --> [*]: Return type
    TypeCheck --> [*]: Return type
Loading

File-Level Changes

Change Details Files
Introduced strawberry.cast to ensure correct type resolution.
  • Added strawberry.cast function.
  • Modified resolve_node and resolve_nodes to use strawberry.cast.
  • Updated is_type_of to check for cast objects.
  • Added tests for strawberry.cast and updated relay field tests.
strawberry/__init__.py
strawberry/relay/fields.py
strawberry/schema/schema_converter.py
strawberry/experimental/pydantic/object_type.py
strawberry/types/cast.py
tests/relay/test_fields.py
tests/types/test_cast.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time. You can also use
    this command to specify where the summary should be inserted.

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@patrick91 patrick91 changed the title fix: Prevent a possible security issue when resolving a relay node with multiple possibilities Add strawberry.cast Jan 9, 2025
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @bellini666 - I've reviewed your changes and they look great!

Here's what I looked at during the review
  • 🟢 General issues: all looks good
  • 🟢 Security: all looks good
  • 🟡 Testing: 1 issue found
  • 🟢 Complexity: all looks good
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

from strawberry.types.cast import is_strawberry_cast_obj


def test_cast():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Test casting with a Strawberry type

Add a test case where the object being cast is already a Strawberry type to ensure idempotency.

Comment on lines +1698 to +1701
if should_have_name:
assert result.data["node"]["name"] == "Strawberry"
else:
assert "name" not in result.data["node"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Avoid conditionals in tests. (no-conditionals-in-tests)

ExplanationAvoid complex code, like conditionals, in test functions.

Google's software engineering guidelines says:
"Clear tests are trivially correct upon inspection"
To reach that avoid complex code in tests:

  • loops
  • conditionals

Some ways to fix this:

  • Use parametrized tests to get rid of the loop.
  • Move the complex logic into helpers.
  • Move the complex part into pytest fixtures.

Complexity is most often introduced in the form of logic. Logic is defined via the imperative parts of programming languages such as operators, loops, and conditionals. When a piece of code contains logic, you need to do a bit of mental computation to determine its result instead of just reading it off of the screen. It doesn't take much logic to make a test more difficult to reason about.

Software Engineering at Google / Don't Put Logic in Tests

@botberry
Copy link
Member

botberry commented Jan 9, 2025

Thanks for adding the RELEASE.md file!

Here's a preview of the changelog:


The common node: Node used to resolve relay nodes means we will be relying on
is_type_of to check if the returned object is in fact a subclass of the Node
interface.

However, integrations such as Django, SQLAlchemy and Pydantic will not return
the type itself, but instead an alike object that is later resolved to the
expected type.

In case there are more than one possible type defined for that model that is
being returned, the first one that replies True to is_type_of check would be
used in the resolution, meaning that when asking for "PublicUser:123",
strawberry could end up returning "User:123", which can lead to security
issues (such as data leakage).

In here we are introducing a new strawberry.cast, which will be used to mark
an object with the already known type by us, and when asking for is_type_of that
mark will be used to check instead, ensuring we will return the correct type.

That cast is already in place for the relay node resolution and pydantic.

Here's the tweet text:

🆕 Release (next) is out! Thanks to @_bellini666 for the PR 👏

Get it here 👉 https://strawberry.rocks/release/(next)

RELEASE.md Show resolved Hide resolved
Copy link

codspeed-hq bot commented Jan 9, 2025

CodSpeed Performance Report

Merging #3749 will not alter performance

Comparing is_type_of_security_issue (fd019d3) with main (fc854f1)

Summary

✅ 21 untouched benchmarks

…th multiple possibilities

The common `node: Node` used to resolve relay nodes means we will be
relying on `is_type_of` to check if the returned object is in fact
a subclass of the `Node` interface.

However, integrations such as Django, SQLAlchemy and Pydantic will
not return the type itself, but instead an alike object that is
later resolved to the expected type.

In case there are more than one possible type defined for that
model that is being returned, the first one that replies `True`
to `is_type_of` check would be used in the resolution, meaning that when
asking for `PublicUser:123`, strawberry could end up returning
`User:123`, which can lead to security issues (such as data leakage).

In here we are introducing a new `strawberry.cast`, which will be used
to mark an object with the already known type by us, and when asking for
`is_type_of` that mark will be used to check instead, ensuring we
will return the correct type.
@bellini666 bellini666 force-pushed the is_type_of_security_issue branch from 0d39a0c to fd019d3 Compare January 9, 2025 18:15
@patrick91 patrick91 merged commit 526eb82 into main Jan 9, 2025
90 of 91 checks passed
@patrick91 patrick91 deleted the is_type_of_security_issue branch January 9, 2025 18:29
Copy link

codecov bot commented Jan 9, 2025

Codecov Report

Attention: Patch coverage is 90.80460% with 8 lines in your changes missing coverage. Please review.

Project coverage is 97.24%. Comparing base (fc854f1) to head (fd019d3).
Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3749      +/-   ##
==========================================
- Coverage   97.28%   97.24%   -0.04%     
==========================================
  Files         502      504       +2     
  Lines       33375    33461      +86     
  Branches     5477     5498      +21     
==========================================
+ Hits        32469    32540      +71     
- Misses        697      703       +6     
- Partials      209      218       +9     

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants