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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +12 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +37 -0
- data/README.md +59 -0
- data/Rakefile +8 -0
- data/lib/rack/cors/csrf_prevention/logger.rb +23 -0
- data/lib/rack/cors/csrf_prevention/version.rb +9 -0
- data/lib/rack/cors/csrf_prevention.rb +76 -0
- data/rack-cors-csrf_prevention.gemspec +56 -0
- metadata +118 -0
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
data/.rubocop.yml
ADDED
data/Gemfile
ADDED
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,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,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: []
|