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

GraphQL mutation allows arbitrary IDs for @belongsTo and @hasOne Relationships Without validation #500

Open
3 tasks done
domov44 opened this issue Jan 15, 2025 · 0 comments
Labels
feature-request New feature or request GraphQL

Comments

@domov44
Copy link

domov44 commented Jan 15, 2025

Before opening, please confirm:

JavaScript Framework

React

Amplify APIs

GraphQL API

Amplify Version

v6

Amplify Categories

storage

Backend

Amplify CLI

Environment information

System:
    OS: Windows 11 10.0.22631
    CPU: (8) x64 Intel(R) Core(TM) i5-10210U CPU @ 1.60GHz
    Memory: 363.64 MB / 7.84 GB
  Binaries:
    Node: 20.10.0 - D:\nodejs\node.EXE
    npm: 10.8.1 - D:\nodejs\npm.CMD
  Browsers:
    Edge: Chromium (127.0.2651.74)
    Internet Explorer: 11.0.22621.3527
  npmPackages:
    @aws-amplify/storage: ^6.0.20 => 6.7.1
    @aws-amplify/storage/internals:  undefined ()
    @aws-amplify/storage/s3:  undefined ()
    @aws-amplify/storage/s3/server:  undefined ()
    @aws-amplify/storage/server:  undefined ()
    @aws-amplify/ui-react: ^6.1.6 => 6.1.6
    @aws-amplify/ui-react-internal:  undefined ()
    @babel/plugin-proposal-private-property-in-object: ^7.21.11 => 7.21.11 (7.21.0-placeholder-for-preset-env.2)
    @cypress/angular:  0.0.0-development
    @cypress/angular-signals:  0.0.0-development
    @cypress/mount-utils:  0.0.0-development
    @cypress/react:  0.0.0-development
    @cypress/react18:  0.0.0-development
    @cypress/svelte:  0.0.0-development
    @cypress/vue:  0.0.0-development
    @cypress/vue2:  0.0.0-development
    @testing-library/jest-dom: ^5.17.0 => 5.17.0
    @testing-library/react: ^13.4.0 => 13.4.0
    @testing-library/user-event: ^13.5.0 => 13.5.0
    aws-amplify: ^6.10.0 => 6.10.0
    aws-amplify/adapter-core:  undefined ()
    aws-amplify/analytics:  undefined ()
    aws-amplify/analytics/kinesis:  undefined ()
    aws-amplify/analytics/kinesis-firehose:  undefined ()
    aws-amplify/analytics/personalize:  undefined ()
    aws-amplify/analytics/pinpoint:  undefined ()
    aws-amplify/api:  undefined ()
    aws-amplify/api/server:  undefined ()
    aws-amplify/auth:  undefined ()
    aws-amplify/auth/cognito:  undefined ()
    aws-amplify/auth/cognito/server:  undefined ()
    aws-amplify/auth/enable-oauth-listener:  undefined ()
    aws-amplify/auth/server:  undefined ()
    aws-amplify/data:  undefined ()
    aws-amplify/data/server:  undefined ()
    aws-amplify/datastore:  undefined ()
    aws-amplify/in-app-messaging:  undefined ()
    aws-amplify/in-app-messaging/pinpoint:  undefined ()
    aws-amplify/push-notifications:  undefined ()
    aws-amplify/push-notifications/pinpoint:  undefined ()
    aws-amplify/storage:  undefined ()
    aws-amplify/storage/s3:  undefined ()
    aws-amplify/storage/s3/server:  undefined ()
    aws-amplify/storage/server:  undefined ()
    aws-amplify/utils:  undefined ()
    aws-sdk: ^2.1692.0 => 2.1692.0
    babel-plugin-styled-components: ^2.1.4 => 2.1.4
    chance: ^1.1.11 => 1.1.11
    cypress: ^13.14.2 => 13.14.2
    dotenv: ^16.4.5 => 16.4.5 (10.0.0)
    file-saver: ^2.0.5 => 2.0.5
    gsap: ^3.12.5 => 3.12.5
    jspdf: ^2.5.2 => 2.5.2
    jspdf-autotable: ^3.8.3 => 3.8.3
    lottie-react: ^2.4.0 => 2.4.0
    react: ^18.3.1 => 18.3.1
    react-content-loader: ^6.2.1 => 6.2.1
    react-content-loader/native:  undefined ()
    react-dom: ^18.3.1 => 18.3.1
    react-icons: ^5.3.0 => 5.3.0
    react-router-dom: ^7.0.1 => 7.0.1
    react-scripts: ^5.0.1 => 5.0.1
    react-toastify: ^10.0.6 => 10.0.6
    styled-components: ^6.1.13 => 6.1.13
    styled-components/native:  undefined ()
    web-vitals: ^4.2.4 => 4.2.4
    xlsx: ^0.18.5 => 0.18.5
  npmGlobalPackages:
    @aws-amplify/cli: 12.12.1
    @nestjs/cli: 10.4.5
    @vue/cli: 5.0.8
    eas-cli: 9.1.0
    expo-cli: 6.3.10
    firebase-tools: 13.1.0
    npm: 10.8.1
    sequelize-cli: 6.6.2


Describe the bug

Maybe someone found the solution, but i didn't find it.

When I use AWS Amplify and GraphQL mutations to create or update entities (for example, a Recipe template), the fields corresponding to relationships defined with directives like @belongsTo or @hasone accept any ID, even if that ID is invalid or belongs to another user or a non-existent category.

For example, in the following mutation to create a recipe, I can provide a category ID (recipeCategoryId) or profile ID (profileID) that :

Does not exist in the database.
Belongs to a user other than the recipe creator.

const newRecipe = await client.graphql({
    query: createRecipe,
    variables: {
        input: {
            title: "test recipe",
            slug: "test-recipe",
            image: "imageKey",
            steps: JSON.stringify([{ description: "Step 1", duration: "5 min", step_number: 1 }]),
            recipeCategoryId: "someInvalidCategoryID",
            profileID: "someOtherUserID",
            description: "This is a test description",
        }
    }
});

In this scenario, Amplify does not automatically validate that :

The category ID provided corresponds to an existing valid category.
The profile ID provided matches the current user profile or a valid profile.
This creates potential security loopholes where a malicious user can intercept the request via a tool like Burp Suite, modify the IDs and associate sensitive data with entities to which they should not have access.

Key issues :
Lack of automatic validation: AppSync does not check the existence or validity of linked IDs during mutations.
Insufficient authorization: It is possible to associate an entity (e.g. a recipe) with another entity belonging to a different user without membership verification.
Technical context:
Directives such as @belongsTo(fields: [“profileID”]) or @hasone only define the structure of relationships between entities. However, they do not provide a built-in check of the IDs sent in a mutation.

To solve this problem, custom logic or a lambda-enhanced authorization strategy must be implemented to validate that :

The IDs supplied exist in the database.
The IDs respect the authorizations defined in the schema.

In this case, however, there's no point in using a mutation and there's no point in using a graphql api.

Expected behavior

I want AWS Amplify to automatically check, during mutations, that the IDs I provide (such as profileID or recipeCategoryId) correspond to existing entities and respect authentication rules. Currently, I can supply any ID, even if it's invalid or belongs to another user, which is a security flaw. I need a solution to prevent this behavior.

Reproduction steps

  1. Install basic configuration of react and amplify
  2. create graphql api with 2 model like "Recipe" and "Profile",
  3. Connect cognito user to profile table with owner rules
  4. Create a Recipe and Profile model with @auth relationships and rules.
  5. Ensure that the Recipe template accepts a profileID field that is linked to the Profile template via an @belongsTo relationship.
  6. Add data to the database via the console or frontend application to create profiles and recipes with valid IDs.
  7. Use the createRecipe mutation, sending valid and invalid IDs for the category or profileID field.
  8. Intercept the request with a tool like Burp Suite or DevTools to manipulate the ID sent in the mutation.
  9. Test whether you can insert an ID that doesn't match any existing profile (for example, by sending an ID from another user, or sending a false category id).

Code Snippet

mutation :

            const newRecipe = await client.graphql({
                query: createRecipe,
                variables: {
                    input: {
                        title: formData1.nom.toLowerCase(),
                        slug: uniqueSlug,
                        image: imageKey,
                        steps: JSON.stringify(formData4.steps.map((step, index) => ({ ...step, step_number: index + 1 }))),
                        recipeCategoryId: formData2.categorie,
                        categoryRecipesId: formData2.categorie,
                        profileID: user.sub,
                        description: formData2.description,
                    }
                }
            });

graphql schemas :

type Profile @model
@auth(rules: [
  {allow: public, provider: identityPool, operations: [read]}
  { allow: owner, ownerField: "owner" },
  { allow: groups, groups: ["Admins"], operations: [read, create, update, delete] },
  { allow: groups, groups: ["Members"], operations: [read] },
]) {
  id: ID! @primaryKey
  pseudo: String! @index(name: "byPseudo", queryField: "profileByPseudo")
  name: String
  surname: String
  avatar: String
  description: String
  birthdate: AWSDate
  recipes: [Recipe] @hasMany
  tags: [Tag] @manyToMany(relationName: "TagProfiles")
  owner: String @auth(rules: [{ allow: groups, groups: ["Admins"] }, { allow: groups, groups: ["Members"], operations: [read] }, { allow: owner, ownerField: "owner", operations: [read] }])
}


type Recipe @model @auth(rules: [
  { allow: public, provider: identityPool, operations: [read] },
  { allow: owner, ownerField: "owner" },
  { allow: groups, groups: ["Admins"], operations: [read, create, update, delete] },
  { allow: groups, groups: ["Members"], operations: [read] }
]) {
  id: ID!
  title: String! @index(name: "byTitle", queryField: "RecipeByTitle")
  slug: String! @index(name: "bySlug", queryField: "RecipeBySlug")
  image: String
  steps: AWSJSON
  description: String
  ingredients: [RecipeIngredient] @hasMany
  category: Category @hasOne
  owner: String @auth(rules: [{ allow: groups, groups: ["Admins"] }, { allow: groups, groups: ["Members"], operations: [read] }, { allow: owner, ownerField: "owner", operations: [read] }])
  profileID: ID!
  user: Profile @belongsTo(fields: ["profileID"])
}


type Ingredient @model @auth(rules: [
  {allow: public, provider: identityPool, operations: [read]}
  {allow: groups, groups: ["Admins"], operations: [read, create, update, delete]},
  {allow: groups, groups: ["Members"], operations: [read]},
]) {
  id: ID!
  name: String!
  typeID: String!
  type: IngredientType @hasOne
}

type IngredientType @model @auth(rules: [
  {allow: public, provider: identityPool, operations: [read]}
  {allow: groups, groups: ["Admins"], operations: [read, create, update, delete]},
  {allow: groups, groups: ["Members"], operations: [read]},
]) {
  id: ID!
  name: String!
}

type Category @model @auth(rules: [
  {allow: public, provider: identityPool, operations: [read]}
  {allow: groups, groups: ["Admins"], operations: [read, create, update, delete]},
  {allow: groups, groups: ["Members"], operations: [read]},
]) {
  id: ID!
  name: String! @index(name: "byName", queryField: "CategoryByname")
  slug: String! @index(name: "bySlug", queryField: "CategoryBySlug")
  recipes: [Recipe] @hasMany
}

type RecipeIngredient @model @auth(rules: [
  {allow: public, provider: identityPool, operations: [read]}
  {allow: groups, groups: ["Admins"], operations: [read, create, update, delete]},
  {allow: groups, groups: ["Members"], operations: [read, create]},
]) {
  id: ID!
  quantity: String
  ingredient: Ingredient @hasOne
}

type Tag @model @auth(rules: [
  {allow: public, provider: identityPool, operations: [read]}
  {allow: groups, groups: ["Admins"], operations: [read, create, update, delete]},
  {allow: groups, groups: ["Members"], operations: [read]},
]) {
  id: ID!
  label: String! @index(name: "byLabel", queryField: "TagByLabel")
  profiles: [Profile] @manyToMany(relationName: "TagProfiles")
}

Log output

// Put your logs below this line


aws-exports.js

/* eslint-disable */
// WARNING: DO NOT EDIT. This file is automatically generated by AWS Amplify. It will be overwritten.

const awsmobile = {
"aws_project_region": "eu-west-3",
"aws_appsync_graphqlEndpoint": "https://4vv5deajfffsna366kzdwv3a74.appsync-api.eu-west-3.amazonaws.com/graphql",
"aws_appsync_region": "eu-west-3",
"aws_appsync_authenticationType": "AMAZON_COGNITO_USER_POOLS",
"aws_appsync_apiKey": "da2-7gmjnjkjzvgohiuz2fr6q3cs4a",
"aws_cloud_logic_custom": [
{
"name": "ragApi",
"endpoint": "https://6cpmav12uk.execute-api.eu-west-3.amazonaws.com/ronantest",
"region": "eu-west-3"
}
],
"aws_cognito_identity_pool_id": "eu-west-3:9f2989b5-c818-4346-bc7b-54a51967b910",
"aws_cognito_region": "eu-west-3",
"aws_user_pools_id": "eu-west-3_8wUfDRtHq",
"aws_user_pools_web_client_id": "25klrfh4o0u47tctmp8fbcdu91",
"oauth": {},
"aws_cognito_username_attributes": [],
"aws_cognito_social_providers": [],
"aws_cognito_signup_attributes": [
"EMAIL"
],
"aws_cognito_mfa_configuration": "OFF",
"aws_cognito_mfa_types": [
"SMS"
],
"aws_cognito_password_protection_settings": {
"passwordPolicyMinLength": 8,
"passwordPolicyCharacters": []
},
"aws_cognito_verification_mechanisms": [
"EMAIL"
],
"aws_user_files_s3_bucket": "recipesappea0631f8364f439bb0004d59ea016d090e0ef-ronantest",
"aws_user_files_s3_bucket_region": "eu-west-3"
};

export default awsmobile;

Manual configuration

No response

Additional configuration

No response

Mobile Device

No response

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

No response

@chrisbonifacio chrisbonifacio transferred this issue from aws-amplify/amplify-js Jan 15, 2025
@chrisbonifacio chrisbonifacio added feature-request New feature or request and removed pending-maintainer-response labels Jan 15, 2025
@domov44 domov44 changed the title GraphQL mutation allows arbitrary IDs for @belongsTo and @hasOne Relationships Without aalidation GraphQL mutation allows arbitrary IDs for @belongsTo and @hasOne Relationships Without validation Jan 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature-request New feature or request GraphQL
Projects
None yet
Development

No branches or pull requests

2 participants