Here's an example of a data leak that occurs if using standard graphql-ruby setup.
Using the graphql nested request below, the response returns data nested under company 1, that belongs to company 2. I expect the response to be limited to the the accountants log that belongs to the company it is nested within.
That way it is, it is leaking information.
The question is how do we patch that leak so the only data returned in the response and its nested objects is data that belongs to the company (the root object).
This query:
query {
company(id: "1") {
id
name
activityLog {
id
activityAt
accountant {
id
name
}
companyId
}
accountants {
id
name
activityLog {
id
activityAt
companyId
}
}
}
}
returns this response:
{
"data": {
"company": {
"id": "1",
"name": "AwsomeCo",
"activityLog": [
{
"id": "1",
"activityAt": "2019-10-12 16:40:13 UTC",
"accountant": {
"id": "100",
"name": "Mr Smith",
},
"companyId": "1"
}
],
"accountants": [
{
"id": "100",
"name": "Mr Smith"
"activityLog": [
{
"id": "1",
"activityAt": "2019-10-12 16:40:13 UTC",
"companyId": "1"
},
{
"id": "2",
"activityAt": "2019-10-11 16:40:13 UTC",
"companyId": "2" // OTHER COMPANY, NEED TO PRESERVE PARENT SCOPE
},
],
}
}
}
}
}
leaking transaction log data of company 2, within the nested elements of company 1.
Again, the question is: How do we preserve scope, only displaying data in context of the company it is displaying?
Code to reproduce:
GraphQL types (using graphql-ruby gem)
#query_type.rb
module Types
class QueryType < Types::BaseObject
# Add root-level fields here.
# They will be entry points for queries on your schema.
field :company_leak, Types::ExampleLeak::CompanyType, null: false do
argument :id, ID, required: true
end
field :companies_leak, [ Types::ExampleLeak::CompanyType ], null: false
def company_leak(id: )
Company.find(id)
end
def companies_leak
Company.all
end
end
end
module Types
module ExampleLeak
class CompanyType < BaseObject
field :id, ID, null: false
field :name, String, null: false
field :transaction_logs, [Types::ExampleLeak::TransactionLogType], null: true
field :accountants, [Types::ExampleLeak::AccountantType], null: true
end
end
end
module Types
module ExampleLeak
class AccountantType < BaseObject
field :id, ID, null: false
field :name, String, null: false
field :transaction_logs, [Types::ExampleLeak::TransactionLogType], null: true
field :companies, [Types::ExampleLeak::CompanyType], null: true
end
end
end
module Types
module ExampleLeak
class TransactionLogType < BaseObject
field :id, ID, null: false
field :activity_at, String, null: false
field :company_id, ID, null: false
field :accountant, Types::ExampleLeak::AccountantType, null: false
end
end
end
Models
class Company < ApplicationRecord
has_and_belongs_to_many :accountants
has_many :transaction_logs
end
class Accountant < ApplicationRecord
has_and_belongs_to_many :companies
has_many :transaction_logs
end
class TransactionLog < ApplicationRecord
belongs_to :company
belongs_to :accountant
end
seeds.rb
awesomeco = Company.create!(name: 'AwesomeCo')
boringco = Company.create!(name: 'BoringCo')
mr_smith = Accountant.create!(name: "Mr. Smith")
awesomeco.accountants << mr_smith
boringco.accountants << mr_smith
mr_smith.transaction_logs.create!(company: awesomeco, activity_at: 1.day.ago)
mr_smith.transaction_logs.create!(company: boringco, activity_at: 2.days.ago)
Public repo containing full code, intended as educational resource:
We can update field in class AccountantType < BaseObject
as follows to resolve transaction logs:
field :transaction_logs, [Types::ExampleLeak::TransactionLogType], null: true,
resolve: ->(obj, args, ctx) {
company_leak = ctx.irep_node.parent.parent.arguments[:id]
companies_leak = ctx.parent.parent.object.object.id
TransactionLog.where(id: company_leak.present? ? company_leak : companies_leak)
}
If the company ID is given an argument, It will fetch transaction logs against that id, otherwise with respect to its parent Accountant