rack-cors-csrf_prevention 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []