rack-cors-csrf_prevention 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 875f008ae969331b4266a00a131b2edf9399d24ea347c2622d9cd79eaa6fcd71
4
+ data.tar.gz: 7de16bb4d69a5783829281c312290c94ec5b544963c45bc03ae38942968eb795
5
+ SHA512:
6
+ metadata.gz: fb20827ff14fa2a57ac931ab263adedc4f87e0422d8ddb1c021abb4878a8023638ded968c05616ab163ce218e14e09ce2026b85101569b8afd25070f31c08635
7
+ data.tar.gz: f8bcbb4e2b5330e27285e3e296d9d2c6db64e6350843e2bb16d0fcef844728120237a81eed4f00f0e426d0fab98c81af349b196288df1d4550298c67a4c1a74c
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,12 @@
1
+ AllCops:
2
+ NewCops: enable
3
+
4
+ Metrics/BlockLength:
5
+ Exclude:
6
+ - *.gemspec
7
+
8
+ Metrics/ParameterLists:
9
+ CountKeywordArgs: false
10
+
11
+ Style/StringLiterals:
12
+ EnforcedStyle: double_quotes
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in rack-cors-csrf_prevention.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,37 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rack-cors-csrf_prevention (0.1.0)
5
+ rack (>= 1)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ diff-lcs (1.5.0)
11
+ rack (2.2.7)
12
+ rake (13.0.6)
13
+ rspec (3.12.0)
14
+ rspec-core (~> 3.12.0)
15
+ rspec-expectations (~> 3.12.0)
16
+ rspec-mocks (~> 3.12.0)
17
+ rspec-core (3.12.1)
18
+ rspec-support (~> 3.12.0)
19
+ rspec-expectations (3.12.2)
20
+ diff-lcs (>= 1.2.0, < 2.0)
21
+ rspec-support (~> 3.12.0)
22
+ rspec-mocks (3.12.4)
23
+ diff-lcs (>= 1.2.0, < 2.0)
24
+ rspec-support (~> 3.12.0)
25
+ rspec-support (3.12.0)
26
+
27
+ PLATFORMS
28
+ arm64-darwin-22
29
+ x86_64-linux
30
+
31
+ DEPENDENCIES
32
+ rack-cors-csrf_prevention!
33
+ rake (~> 13.0)
34
+ rspec (~> 3.0)
35
+
36
+ BUNDLED WITH
37
+ 2.4.4
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # Rack::Cors::CsrfPrevention
2
+
3
+ Ruby implementation of [CSRF prevention from the Apollo Router](https://www.apollographql.com/docs/router/configuration/csrf/).
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ ```shell
10
+ bundle add rack-cors-csrf_prevention
11
+ ```
12
+
13
+ If bundler is not being used to manage dependencies, install the gem by executing:
14
+
15
+ ```shell
16
+ gem install rack-cors-csrf_prevention
17
+ ```
18
+
19
+ ## Configuration
20
+
21
+ ### Rails Configuration
22
+
23
+ Specify paths for CSRF prevention:
24
+
25
+ ```ruby
26
+ # config/initializers/cors.rb
27
+
28
+ Rails.application.config.middleware.use Rack::Cors::CsrfPrevention,
29
+ paths: %w[/graphql]
30
+ ```
31
+
32
+ You can also specify custom headers that allow execution. By default, it's `X-Apollo-Operation-Name` or `Apollo-Require-Preflight` headers, but you can configure to allow a `Some-Special-Header` header:
33
+
34
+ ```ruby
35
+ # config/initializers/cors.rb
36
+
37
+ Rails.application.config.middleware.use Rack::Cors::CsrfPrevention,
38
+ paths: %w[/graphql],
39
+ required_headers: %w[
40
+ X-APOLLO-OPERATION-NAME
41
+ APOLLO-REQUIRE-PREFLIGHT
42
+ SOME-SPECIAL-HEADER
43
+ ]
44
+ ```
45
+
46
+ ## Development
47
+
48
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
49
+ `bundle exec rake spec` to run the tests. You can also run `bin/console` for an
50
+ interactive prompt that will allow you to experiment.
51
+
52
+ To install this gem onto your local machine, run `bundle exec rake install`.
53
+ To release a new version, update the version number in `version.rb`, and then
54
+ run `bundle exec rake release`, which will create a git tag for the version,
55
+ push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
56
+
57
+ ## Contributing
58
+
59
+ Bug reports and pull requests are welcome on GitHub at https://github.com/digitaz/rack-cors-csrf_prevention.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Rack
6
+ class Cors
7
+ class CsrfPrevention
8
+ module Logger
9
+ def logger(env)
10
+ @logger = if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
11
+ Rails.logger
12
+ elsif env[RACK_LOGGER]
13
+ env[RACK_LOGGER]
14
+ else
15
+ ::Logger.new($stdout).tap do |logger|
16
+ logger.level = ::Logger::Severity::DEBUG
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class Cors
5
+ class CsrfPrevention
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack"
4
+ require_relative "csrf_prevention/logger"
5
+ require_relative "csrf_prevention/version"
6
+
7
+ module Rack
8
+ class Cors
9
+ class CsrfPrevention
10
+ include Rack::Cors::CsrfPrevention::Logger
11
+
12
+ APOLLO_CUSTOM_PREFLIGHT_HEADERS = %w[
13
+ X-APOLLO-OPERATION-NAME
14
+ APOLLO-REQUIRE-PREFLIGHT
15
+ ].freeze
16
+
17
+ NON_PREFLIGHTED_CONTENT_TYPES = %w[
18
+ application/x-www-form-urlencoded
19
+ multipart/form-data
20
+ text/plain
21
+ ].freeze
22
+
23
+ ERROR_MESSAGE = <<~HEREDOC
24
+ This operation has been blocked as a potential Cross-Site Request Forgery (CSRF).
25
+
26
+ Please either specify a "Content-Type" header (with a mime-type that is not one of #{NON_PREFLIGHTED_CONTENT_TYPES.join(', ')}) or provide one of the following headers: #{APOLLO_CUSTOM_PREFLIGHT_HEADERS.join(', ')}.
27
+ HEREDOC
28
+
29
+ def initialize(
30
+ app,
31
+ paths:,
32
+ required_headers: APOLLO_CUSTOM_PREFLIGHT_HEADERS
33
+ )
34
+ @app = app
35
+ @paths = paths
36
+ @required_headers = required_headers
37
+ end
38
+
39
+ def call(env)
40
+ request = ::Rack::Request.new(env)
41
+
42
+ return @app.call(env) unless protected_path?(request.path)
43
+
44
+ if preflighted?(request)
45
+ logger(env).debug { "Request is preflighted" }
46
+
47
+ @app.call(env)
48
+ else
49
+ logger(env).debug { "Request isn't preflighted" }
50
+
51
+ Rack::Response[400, { "Content-Type" => "text/plain" }, ERROR_MESSAGE].to_a
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def protected_path?(path)
58
+ @paths.include?(path)
59
+ end
60
+
61
+ def preflighted?(request)
62
+ content_type_requires_preflight?(request) || recommended_header_provided?(request)
63
+ end
64
+
65
+ def content_type_requires_preflight?(request)
66
+ return false unless request.media_type
67
+
68
+ !NON_PREFLIGHTED_CONTENT_TYPES.include?(request.media_type)
69
+ end
70
+
71
+ def recommended_header_provided?(request)
72
+ @required_headers.any? { |header| request.has_header?("HTTP_#{header}") }
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/rack/cors/csrf_prevention/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "rack-cors-csrf_prevention"
7
+ spec.version = Rack::Cors::CsrfPrevention::VERSION
8
+ spec.author = "Digital Classifieds LLC"
9
+ spec.license = "MIT"
10
+
11
+ spec.summary = "Ruby implementation of CSRF prevention from the Apollo Router."
12
+ spec.description = <<~HEREDOC
13
+ The middleware makes sure any request to specified paths would have been
14
+ preflighted if it was sent by a browser.
15
+
16
+ We don't want random websites to be able to execute actual GraphQL
17
+ operations from a user's browser unless our CORS policy supports it. It's
18
+ not good enough just to ensure that the browser can't read the response from
19
+ the operation; we also want to prevent CSRF, where the attacker can cause
20
+ side effects with an operation or can measure the timing of a read
21
+ operation. Our goal is to ensure that we don't run the context function or
22
+ execute the GraphQL operation until the browser has evaluated the CORS
23
+ policy, which means we want all operations to be pre-flighted. We can do
24
+ that by only processing operations that have at least one header set that
25
+ appears to be manually set by the JS code rather than by the browser
26
+ automatically.
27
+
28
+ POST requests generally have a content-type `application/json`, which is
29
+ sufficient to trigger preflighting. So we take extra care with requests that
30
+ specify no content-type or that specify one of the three non-preflighted
31
+ content types. For those operations, we require one of a set of specific
32
+ headers to be set. By ensuring that every operation either has a custom
33
+ content-type or sets one of these headers, we know we won't execute
34
+ operations at the request of origins who our CORS policy will block.
35
+ HEREDOC
36
+ spec.homepage = "https://github.com/digitaz/rack-cors-csrf_prevention"
37
+ spec.required_ruby_version = ">= 2.6.0"
38
+
39
+ spec.metadata["homepage_uri"] = spec.homepage
40
+ spec.metadata["source_code_uri"] = spec.homepage
41
+
42
+ # Specify which files should be added to the gem when it is released.
43
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
44
+ spec.files = Dir.chdir(__dir__) do
45
+ `git ls-files -z`.split("\x0").reject do |f|
46
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
47
+ end
48
+ end
49
+ spec.require_paths = ["lib"]
50
+
51
+ spec.add_dependency "rack", ">= 1"
52
+
53
+ spec.add_development_dependency "rake", "~> 13.0"
54
+ # spec.add_development_dependency "minitest", "~> 5.0"
55
+ spec.add_development_dependency "rspec", "~> 3.0"
56
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-cors-csrf_prevention
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Digital Classifieds LLC
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-02-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: |
56
+ The middleware makes sure any request to specified paths would have been
57
+ preflighted if it was sent by a browser.
58
+
59
+ We don't want random websites to be able to execute actual GraphQL
60
+ operations from a user's browser unless our CORS policy supports it. It's
61
+ not good enough just to ensure that the browser can't read the response from
62
+ the operation; we also want to prevent CSRF, where the attacker can cause
63
+ side effects with an operation or can measure the timing of a read
64
+ operation. Our goal is to ensure that we don't run the context function or
65
+ execute the GraphQL operation until the browser has evaluated the CORS
66
+ policy, which means we want all operations to be pre-flighted. We can do
67
+ that by only processing operations that have at least one header set that
68
+ appears to be manually set by the JS code rather than by the browser
69
+ automatically.
70
+
71
+ POST requests generally have a content-type `application/json`, which is
72
+ sufficient to trigger preflighting. So we take extra care with requests that
73
+ specify no content-type or that specify one of the three non-preflighted
74
+ content types. For those operations, we require one of a set of specific
75
+ headers to be set. By ensuring that every operation either has a custom
76
+ content-type or sets one of these headers, we know we won't execute
77
+ operations at the request of origins who our CORS policy will block.
78
+ email:
79
+ executables: []
80
+ extensions: []
81
+ extra_rdoc_files: []
82
+ files:
83
+ - ".rspec"
84
+ - ".rubocop.yml"
85
+ - Gemfile
86
+ - Gemfile.lock
87
+ - README.md
88
+ - Rakefile
89
+ - lib/rack/cors/csrf_prevention.rb
90
+ - lib/rack/cors/csrf_prevention/logger.rb
91
+ - lib/rack/cors/csrf_prevention/version.rb
92
+ - rack-cors-csrf_prevention.gemspec
93
+ homepage: https://github.com/digitaz/rack-cors-csrf_prevention
94
+ licenses:
95
+ - MIT
96
+ metadata:
97
+ homepage_uri: https://github.com/digitaz/rack-cors-csrf_prevention
98
+ source_code_uri: https://github.com/digitaz/rack-cors-csrf_prevention
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: 2.6.0
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubygems_version: 3.3.26
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: Ruby implementation of CSRF prevention from the Apollo Router.
118
+ test_files: []