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

[APPSEC-9341] Allow blocking response template configuration via ENV variables #2975

Merged
merged 2 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions lib/datadog/appsec/configuration/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def self.extended(base)
add_settings!(base)
end

# rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/BlockLength
# rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/BlockLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
def self.add_settings!(base)
base.class_eval do
settings :appsec do
Expand Down Expand Up @@ -95,6 +95,46 @@ def self.add_settings!(base)
o.default DEFAULT_OBFUSCATOR_VALUE_REGEX
end

settings :block do
settings :templates do
option :html do |o|
o.env 'DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML'
o.type :string, nilable: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to provide nilable because we use using_default? when checking whether we need to use the default value.

https://github.com/DataDog/dd-trace-rb/pull/2975/files#diff-83b38af05def4a7362ff8abda84f27d84964c9fce7781eb7c4a27ecbfc235c66R77

Since using_default? computes the option, we needed to provide something that would make it work. Another solution would be to give an empty string as default.

o.setter do |value|
if value
raise(ArgumentError, "appsec.templates.html: file not found: #{value}") unless File.exist?(value)

File.open(value, 'rb', &:read) || ''
end
end
end

option :json do |o|
o.env 'DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON'
o.type :string, nilable: true
o.setter do |value|
if value
raise(ArgumentError, "appsec.templates.json: file not found: #{value}") unless File.exist?(value)

File.open(value, 'rb', &:read) || ''
end
end
end

option :text do |o|
o.env 'DD_APPSEC_HTTP_BLOCKED_TEMPLATE_TEXT'
o.type :string, nilable: true
o.setter do |value|
if value
raise(ArgumentError, "appsec.templates.text: file not found: #{value}") unless File.exist?(value)

File.open(value, 'rb', &:read) || ''
end
end
end
end
end

settings :track_user_events do
option :enabled do |o|
o.default true
Expand Down Expand Up @@ -132,7 +172,7 @@ def self.add_settings!(base)
end
end
end
# rubocop:enable Metrics/AbcSize,Metrics/MethodLength,Metrics/BlockLength
# rubocop:enable Metrics/AbcSize,Metrics/MethodLength,Metrics/BlockLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
end
end
end
Expand Down
17 changes: 16 additions & 1 deletion lib/datadog/appsec/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ def negotiate(env)

Datadog.logger.debug { "negotiated response content type: #{content_type}" }

body = []
body << content(content_type)

Response.new(
status: 403,
headers: { 'Content-Type' => content_type },
body: [Datadog::AppSec::Assets.blocked(format: CONTENT_TYPE_TO_FORMAT[content_type])]
body: body,
)
end

Expand Down Expand Up @@ -67,6 +70,18 @@ def content_type(env)
rescue Datadog::AppSec::Utils::HTTP::MediaRange::ParseError
DEFAULT_CONTENT_TYPE
end

def content(content_type)
content_format = CONTENT_TYPE_TO_FORMAT[content_type]

using_default = Datadog.configuration.appsec.block.templates.using_default?(content_format)

if using_default
Datadog::AppSec::Assets.blocked(format: content_format)
else
Datadog.configuration.appsec.block.templates.send(content_format)
end
end
end
end
end
Expand Down
7 changes: 4 additions & 3 deletions sig/datadog/appsec/response.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ module Datadog
class Response
attr_reader status: ::Integer
attr_reader headers: ::Hash[::String, ::String]
attr_reader body: ::Array[untyped]
attr_reader body: ::Array[::String]

def initialize: (status: ::Integer, ?headers: ::Hash[::String, ::String], ?body: ::Array[untyped]) -> void

def initialize: (status: ::Integer, ?headers: ::Hash[::String, ::String], ?body: ::Array[::String]) -> void
def to_rack: () -> ::Array[untyped]
def to_sinatra_response: () -> ::Sinatra::Response
def to_action_dispatch_response: () -> ::ActionDispatch::Response
Expand All @@ -17,8 +18,8 @@ module Datadog
CONTENT_TYPE_TO_FORMAT: ::Hash[::String, ::Symbol]
DEFAULT_CONTENT_TYPE: ::String

def self.format: (::Hash[untyped, untyped] env) -> ::Symbol
def self.content_type: (::Hash[untyped, untyped] env) -> ::String
def self.content: (::String) -> ::String
end
end
end
20 changes: 19 additions & 1 deletion sig/datadog/core/configuration/settings.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,25 @@ module Datadog

def ruleset=: (String | Symbol | File | StringIO | ::Hash[untyped, untyped]) -> void

def using_default?: (Symbol option) -> bool
def block: () -> _AppSecBlock
end

interface _AppSecBlock
def templates: () -> _TemplatesBlock
end

interface _TemplatesBlock
def html=: (::String) -> void

def html: () -> ::String

def json=: (::String) -> void

def json: () -> ::String

def text=: (::String) -> void

def text: () -> ::String
end

def initialize: (*untyped _) -> untyped
Expand Down
74 changes: 74 additions & 0 deletions spec/datadog/appsec/configuration/settings_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -559,5 +559,79 @@ def patcher
end
end
end

describe 'block' do
describe 'templates' do
[
{ method_name: :html, env_var: 'DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML' },
{ method_name: :json, env_var: 'DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON' },
{ method_name: :text, env_var: 'DD_APPSEC_HTTP_BLOCKED_TEMPLATE_TEXT' }
].each do |test_info|
describe "##{test_info[:method_name]}" do
context "when #{test_info[:env_var]}" do
subject(:template) { settings.appsec.block.templates.send(test_info[:method_name]) }

around do |example|
ClimateControl.modify(test_info[:env_var] => template_path) do
example.run
end
end

context 'is defined and the file exists' do
before do
File.write(template_path, 'testing')
end

after do
File.delete(template_path)
end

let(:template_path) do
"hello.#{test_info[:method_name]}"
end

it { is_expected.to eq 'testing' }
end

context 'is defined and the file do not exists' do
let(:template_path) do
"hello.#{test_info[:method_name]}"
end

it { expect { is_expected }.to raise_error(ArgumentError) }
end
end
end

describe "##{test_info[:method_name]}=" do
subject(:template) { settings.appsec.block.templates.send("#{test_info[:method_name]}=", template_path) }

context 'is defined and the file exists' do
before do
File.write(template_path, 'testing')
end

after do
File.delete(template_path)
end

let(:template_path) do
"hello.#{test_info[:method_name]}"
end

it { is_expected.to eq 'testing' }
end

context 'is defined and the file do not exists' do
let(:template_path) do
"hello.#{test_info[:method_name]}"
end

it { expect { is_expected }.to raise_error(ArgumentError) }
end
end
end
end
end
end
end
28 changes: 27 additions & 1 deletion spec/datadog/appsec/response_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
end

describe '.status' do
subject(:content_type) { described_class.negotiate(env).status }
subject(:status) { described_class.negotiate(env).status }

it { is_expected.to eq 403 }
end
Expand All @@ -23,22 +23,48 @@
expect(env).to receive(:[]).with('HTTP_ACCEPT').and_return(accept)
end

shared_examples_for 'with custom response body' do |type|
before do
File.write("test.#{type}", 'testing')
Datadog.configuration.appsec.block.templates.send("#{type}=", "test.#{type}")
end

after do
File.delete("test.#{type}")
Datadog.configuration.appsec.reset!
end

it { is_expected.to eq ['testing'] }
end

context 'with unsupported Accept headers' do
let(:accept) { 'application/xml' }

it { is_expected.to eq [Datadog::AppSec::Assets.blocked(format: :json)] }
end

context('with Accept: text/html') do
let(:accept) { 'text/html' }

it { is_expected.to eq [Datadog::AppSec::Assets.blocked(format: :html)] }

it_behaves_like 'with custom response body', :html
end

context('with Accept: application/json') do
let(:accept) { 'application/json' }

it { is_expected.to eq [Datadog::AppSec::Assets.blocked(format: :json)] }

it_behaves_like 'with custom response body', :json
end

context('with Accept: text/plain') do
let(:accept) { 'text/plain' }

it { is_expected.to eq [Datadog::AppSec::Assets.blocked(format: :text)] }

it_behaves_like 'with custom response body', :text
end
end

Expand Down
Loading