Skip to content

Commit bb2b04f

Browse files
committed
Add security response id to AppSec blocking response
This unique identifier, introduced in `libddwaf` v1.28.0, can be used to correlate blocked requests with logs, traces, and security events.
1 parent 49cee89 commit bb2b04f

File tree

6 files changed

+91
-16
lines changed

6 files changed

+91
-16
lines changed

lib/datadog/appsec/assets/blocked.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,20 @@
8282
footer p {
8383
font-size: 16px
8484
}
85+
86+
.security-response-id {
87+
font-size:14px;
88+
color:#999;
89+
margin-top:20px;
90+
font-family:monospace
91+
}
8592
</style>
8693
</head>
8794

8895
<body>
8996
<main>
9097
<p>Sorry, you cannot access this page. Please contact the customer service team.</p>
98+
<p class="security-response-id">Security Response ID: [security_response_id]</p>
9199
</main>
92100
<footer>
93101
<p>Security provided by <a
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"errors": [{"title": "You've been blocked", "detail": "Sorry, you cannot access this page. Please contact the customer service team. Security provided by Datadog."}]}
1+
{"errors":[{"title":"You've been blocked","detail":"Sorry, you cannot access this page. Please contact the customer service team. Security provided by Datadog."}],"security_response_id":"[security_response_id]"}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
You've been blocked
1+
You've been blocked.
22

33
Sorry, you cannot access this page. Please contact the customer service team.
44

5+
Security Response ID: [security_response_id]
6+
57
Security provided by Datadog.

lib/datadog/appsec/response.rb

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ module Datadog
77
module AppSec
88
# AppSec response
99
class Response
10+
SECURITY_RESPONSE_ID_PLACEHOLDER = '[security_response_id]'
11+
1012
attr_reader :status, :headers, :body
1113

1214
def initialize(status:, headers: {}, body: [])
@@ -37,7 +39,12 @@ def block_response(interrupt_params, http_accept_header)
3739
Response.new(
3840
status: interrupt_params['status_code']&.to_i || 403,
3941
headers: {'Content-Type' => content_type},
40-
body: [content(content_type)],
42+
body: [
43+
content(
44+
security_response_id: interrupt_params['security_response_id'],
45+
content_type: content_type
46+
)
47+
],
4148
)
4249
end
4350

@@ -82,16 +89,20 @@ def content_type(http_accept_header)
8289
DEFAULT_CONTENT_TYPE
8390
end
8491

85-
def content(content_type)
92+
def content(security_response_id:, content_type:)
8693
content_format = CONTENT_TYPE_TO_FORMAT[content_type]
8794

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

90-
if using_default
97+
template = if using_default
9198
Datadog::AppSec::Assets.blocked(format: content_format)
9299
else
93100
Datadog.configuration.appsec.block.templates.send(content_format)
94101
end
102+
103+
# we want to return a new string here to avoid mutating the template,
104+
# since it is memoized in AppSec::Assets module
105+
template.gsub(SECURITY_RESPONSE_ID_PLACEHOLDER, security_response_id.to_s)
95106
end
96107
end
97108
end

sig/datadog/appsec/response.rbs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module Datadog
22
module AppSec
33
class Response
4+
SECURITY_RESPONSE_ID_PLACEHOLDER: ::String
5+
46
attr_reader status: ::Integer
57
attr_reader headers: ::Hash[::String, ::String]
68
attr_reader body: ::Array[::String]
@@ -21,7 +23,7 @@ module Datadog
2123
def self.redirect_response: (::Hash[::String, ::String] interrupt_params) -> Response
2224

2325
def self.content_type: (::String) -> ::String
24-
def self.content: (::String) -> ::String
26+
def self.content: (security_response_id: ::String, content_type: ::String) -> ::String
2527
end
2628
end
2729
end

spec/datadog/appsec/response_spec.rb

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'securerandom'
2+
13
require 'datadog/appsec/response'
24

35
RSpec.describe Datadog::AppSec::Response do
@@ -9,12 +11,14 @@
911
let(:interrupt_params) do
1012
{
1113
'type' => type,
12-
'status_code' => status_code
14+
'status_code' => status_code,
15+
'security_response_id' => security_response_id
1316
}
1417
end
1518

1619
let(:type) { 'html' }
1720
let(:status_code) { '100' }
21+
let(:security_response_id) { SecureRandom.uuid }
1822

1923
context 'status_code' do
2024
subject(:status) { described_class.from_interrupt_params(interrupt_params, http_accept_header).status }
@@ -31,13 +35,25 @@
3135
context 'body' do
3236
subject(:body) { described_class.from_interrupt_params(interrupt_params, http_accept_header).body }
3337

34-
it { is_expected.to eq [Datadog::AppSec::Assets.blocked(format: :html)] }
38+
it 'returns response template with substituted [security_response_id]' do
39+
expect(body).to eq([
40+
Datadog::AppSec::Assets
41+
.blocked(format: :html)
42+
.gsub(Datadog::AppSec::Response::SECURITY_RESPONSE_ID_PLACEHOLDER, security_response_id)
43+
])
44+
end
3545

3646
context 'type is auto it uses the HTTP_ACCEPT to decide the result' do
3747
let(:type) { 'auto' }
3848
let(:http_accept_header) { 'application/json' }
3949

40-
it { is_expected.to eq [Datadog::AppSec::Assets.blocked(format: :json)] }
50+
it 'returns the response body with correct content type' do
51+
expect(body).to eq([
52+
Datadog::AppSec::Assets
53+
.blocked(format: :json)
54+
.gsub(Datadog::AppSec::Response::SECURITY_RESPONSE_ID_PLACEHOLDER, security_response_id)
55+
])
56+
end
4157
end
4258
end
4359

@@ -60,10 +76,15 @@
6076
let(:interrupt_params) { {} }
6177
subject(:response) { described_class.from_interrupt_params(interrupt_params, http_accept_header) }
6278

63-
it 'uses default response' do
79+
it 'uses default response and removes [security_response_id] from the template' do
6480
expect(response.status).to eq 403
65-
expect(response.body).to eq [Datadog::AppSec::Assets.blocked(format: :html)]
6681
expect(response.headers['Content-Type']).to eq 'text/html'
82+
83+
expect(response.body).to eq([
84+
Datadog::AppSec::Assets
85+
.blocked(format: :html)
86+
.gsub(Datadog::AppSec::Response::SECURITY_RESPONSE_ID_PLACEHOLDER, '')
87+
])
6788
end
6889
end
6990
end
@@ -116,7 +137,14 @@
116137
end
117138

118139
describe '.body' do
119-
subject(:body) { described_class.from_interrupt_params({}, http_accept_header).body }
140+
let(:security_response_id) { SecureRandom.uuid }
141+
142+
subject(:body) do
143+
described_class.from_interrupt_params(
144+
{'security_response_id' => security_response_id},
145+
http_accept_header
146+
).body
147+
end
120148

121149
shared_examples_for 'with custom response body' do |type|
122150
before do
@@ -135,29 +163,53 @@
135163
context 'with unsupported Accept headers' do
136164
let(:http_accept_header) { 'application/xml' }
137165

138-
it { is_expected.to eq [Datadog::AppSec::Assets.blocked(format: :json)] }
166+
it 'returns default json template with substituted security_response_id' do
167+
expect(body).to eq([
168+
Datadog::AppSec::Assets
169+
.blocked(format: :json)
170+
.gsub(Datadog::AppSec::Response::SECURITY_RESPONSE_ID_PLACEHOLDER, security_response_id)
171+
])
172+
end
139173
end
140174

141175
context('with Accept: text/html') do
142176
let(:http_accept_header) { 'text/html' }
143177

144-
it { is_expected.to eq [Datadog::AppSec::Assets.blocked(format: :html)] }
178+
it 'returns default html template with substituted security_response_id' do
179+
expect(body).to eq([
180+
Datadog::AppSec::Assets
181+
.blocked(format: :html)
182+
.gsub(Datadog::AppSec::Response::SECURITY_RESPONSE_ID_PLACEHOLDER, security_response_id)
183+
])
184+
end
145185

146186
it_behaves_like 'with custom response body', :html
147187
end
148188

149189
context('with Accept: application/json') do
150190
let(:http_accept_header) { 'application/json' }
151191

152-
it { is_expected.to eq [Datadog::AppSec::Assets.blocked(format: :json)] }
192+
it 'returns default json template with substituted security_response_id' do
193+
expect(body).to eq([
194+
Datadog::AppSec::Assets
195+
.blocked(format: :json)
196+
.gsub(Datadog::AppSec::Response::SECURITY_RESPONSE_ID_PLACEHOLDER, security_response_id)
197+
])
198+
end
153199

154200
it_behaves_like 'with custom response body', :json
155201
end
156202

157203
context('with Accept: text/plain') do
158204
let(:http_accept_header) { 'text/plain' }
159205

160-
it { is_expected.to eq [Datadog::AppSec::Assets.blocked(format: :text)] }
206+
it 'returns default text template with substituted security_response_id' do
207+
expect(body).to eq([
208+
Datadog::AppSec::Assets
209+
.blocked(format: :text)
210+
.gsub(Datadog::AppSec::Response::SECURITY_RESPONSE_ID_PLACEHOLDER, security_response_id)
211+
])
212+
end
161213

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

0 commit comments

Comments
 (0)