Skip to content

Commit f581fcc

Browse files
tipabuAlistair Coles
authored andcommitted
By default, disallow inbound X-Timestamp headers
With the X-Timestamp validation added in commit e619411, end users could upload objects with X-Timestamp: 9999999999.99999_ffffffffffffffff (the maximum value) and Swift would be unable to delete them. Now, inbound X-Timestamp headers will be moved to X-Backend-Inbound-X-Timestamp, effectively rendering them harmless. The primary reason to allow X-Timestamp before was to prevent Last-Modified changes for objects coming from either: * container_sync or * a migration from another storage system. To enable the former use-case, the container_sync middleware will now translate X-Backend-Inbound-X-Timestamp headers back to X-Timestamp after verifying the request. Additionally, a new option is added to the gatekeeper filter config: # shunt_inbound_x_timestamp = true To enable the latter use-case (or any other use-case not mentioned), set this to false. Upgrade Consideration ===================== If your cluster workload requires that clients be allowed to specify objects' X-Timestamp values, disable the shunt_inbound_x_timestamp option before upgrading. UpgradeImpact Change-Id: I8799d5eb2ae9d795ba358bb422f69c70ee8ebd2c
1 parent 11962b8 commit f581fcc

File tree

6 files changed

+122
-17
lines changed

6 files changed

+122
-17
lines changed

etc/proxy-server.conf-sample

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,12 @@ use = egg:swift#account_quotas
674674

675675
[filter:gatekeeper]
676676
use = egg:swift#gatekeeper
677+
# Set this to false if you want to allow clients to set arbitrary X-Timestamps
678+
# on uploaded objects. This may be used to preserve timestamps when migrating
679+
# from a previous storage system, but risks allowing users to upload
680+
# difficult-to-delete data.
681+
# shunt_inbound_x_timestamp = true
682+
#
677683
# You can override the default log routing for this filter here:
678684
# set log_name = gatekeeper
679685
# set log_facility = LOG_LOCAL0

swift/common/middleware/container_sync.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ def __call__(self, req):
9797
req.environ.setdefault('swift.log_info', []).append(
9898
'cs:no-local-user-key')
9999
else:
100+
# x-timestamp headers get shunted by gatekeeper
101+
if 'x-backend-inbound-x-timestamp' in req.headers:
102+
req.headers['x-timestamp'] = req.headers.pop(
103+
'x-backend-inbound-x-timestamp')
104+
100105
expected = self.realms_conf.get_sig(
101106
req.method, req.path,
102107
req.headers.get('x-timestamp', '0'), nonce,

swift/common/middleware/gatekeeper.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333

3434
from swift.common.swob import Request
35-
from swift.common.utils import get_logger
35+
from swift.common.utils import get_logger, config_true_value
3636
from swift.common.request_helpers import remove_items, get_sys_meta_prefix
3737
import re
3838

@@ -69,13 +69,22 @@ def __init__(self, app, conf):
6969
self.logger = get_logger(conf, log_route='gatekeeper')
7070
self.inbound_condition = make_exclusion_test(inbound_exclusions)
7171
self.outbound_condition = make_exclusion_test(outbound_exclusions)
72+
self.shunt_x_timestamp = config_true_value(
73+
conf.get('shunt_inbound_x_timestamp', 'true'))
7274

7375
def __call__(self, env, start_response):
7476
req = Request(env)
7577
removed = remove_items(req.headers, self.inbound_condition)
7678
if removed:
7779
self.logger.debug('removed request headers: %s' % removed)
7880

81+
if 'X-Timestamp' in req.headers and self.shunt_x_timestamp:
82+
ts = req.headers.pop('X-Timestamp')
83+
req.headers['X-Backend-Inbound-X-Timestamp'] = ts
84+
# log in a similar format as the removed headers
85+
self.logger.debug('shunted request headers: %s' %
86+
[('X-Timestamp', ts)])
87+
7988
def gatekeeper_response(status, response_headers, exc_info=None):
8089
removed = filter(
8190
lambda h: self.outbound_condition(h[0]),

test/functional/test_object.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -167,11 +167,28 @@ def put(url, token, parsed, conn):
167167
'Content-Length': '0',
168168
'X-Timestamp': '-1'})
169169
return check_response(conn)
170+
171+
def head(url, token, parsed, conn):
172+
conn.request('HEAD', '%s/%s/%s' % (parsed.path, self.container,
173+
'too_small_x_timestamp'),
174+
'', {'X-Auth-Token': token,
175+
'Content-Length': '0'})
176+
return check_response(conn)
177+
ts_before = time.time()
170178
resp = retry(put)
171179
body = resp.read()
172-
self.assertEqual(resp.status, 400)
173-
self.assertIn(
174-
'X-Timestamp should be a UNIX timestamp float value', body)
180+
ts_after = time.time()
181+
if resp.status == 400:
182+
# shunt_inbound_x_timestamp must be false
183+
self.assertIn(
184+
'X-Timestamp should be a UNIX timestamp float value', body)
185+
else:
186+
self.assertEqual(resp.status, 201)
187+
self.assertEqual(body, '')
188+
resp = retry(head)
189+
resp.read()
190+
self.assertGreater(float(resp.headers['x-timestamp']), ts_before)
191+
self.assertLess(float(resp.headers['x-timestamp']), ts_after)
175192

176193
def test_too_big_x_timestamp(self):
177194
def put(url, token, parsed, conn):
@@ -181,11 +198,28 @@ def put(url, token, parsed, conn):
181198
'Content-Length': '0',
182199
'X-Timestamp': '99999999999.9999999999'})
183200
return check_response(conn)
201+
202+
def head(url, token, parsed, conn):
203+
conn.request('HEAD', '%s/%s/%s' % (parsed.path, self.container,
204+
'too_big_x_timestamp'),
205+
'', {'X-Auth-Token': token,
206+
'Content-Length': '0'})
207+
return check_response(conn)
208+
ts_before = time.time()
184209
resp = retry(put)
185210
body = resp.read()
186-
self.assertEqual(resp.status, 400)
187-
self.assertIn(
188-
'X-Timestamp should be a UNIX timestamp float value', body)
211+
ts_after = time.time()
212+
if resp.status == 400:
213+
# shunt_inbound_x_timestamp must be false
214+
self.assertIn(
215+
'X-Timestamp should be a UNIX timestamp float value', body)
216+
else:
217+
self.assertEqual(resp.status, 201)
218+
self.assertEqual(body, '')
219+
resp = retry(head)
220+
resp.read()
221+
self.assertGreater(float(resp.headers['x-timestamp']), ts_before)
222+
self.assertLess(float(resp.headers['x-timestamp']), ts_after)
189223

190224
def test_x_delete_after(self):
191225
def put(url, token, parsed, conn):

test/unit/common/middleware/test_container_sync.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ def __call__(self, env, start_response):
4242
body = 'Response to Authorized Request'
4343
else:
4444
body = 'Pass-Through Response'
45-
start_response('200 OK', [('Content-Length', str(len(body)))])
45+
headers = [('Content-Length', str(len(body)))]
46+
if 'HTTP_X_TIMESTAMP' in env:
47+
headers.append(('X-Timestamp', env['HTTP_X_TIMESTAMP']))
48+
start_response('200 OK', headers)
4649
return body
4750

4851

@@ -214,18 +217,20 @@ def test_invalid_sig(self):
214217
req.environ.get('swift.log_info'))
215218

216219
def test_valid_sig(self):
220+
ts = '1455221706.726999_0123456789abcdef'
217221
sig = self.sync.realms_conf.get_sig(
218-
'GET', '/v1/a/c', '0', 'nonce',
222+
'GET', '/v1/a/c', ts, 'nonce',
219223
self.sync.realms_conf.key('US'), 'abc')
220-
req = swob.Request.blank(
221-
'/v1/a/c', headers={'x-container-sync-auth': 'US nonce ' + sig})
224+
req = swob.Request.blank('/v1/a/c', headers={
225+
'x-container-sync-auth': 'US nonce ' + sig,
226+
'x-backend-inbound-x-timestamp': ts})
222227
req.environ[_get_cache_key('a', 'c')[1]] = {'sync_key': 'abc'}
223228
resp = req.get_response(self.sync)
224229
self.assertEqual(resp.status, '200 OK')
225230
self.assertEqual(resp.body, 'Response to Authorized Request')
226-
self.assertTrue(
227-
'cs:valid' in req.environ.get('swift.log_info'),
228-
req.environ.get('swift.log_info'))
231+
self.assertIn('cs:valid', req.environ.get('swift.log_info'))
232+
self.assertIn('X-Timestamp', resp.headers)
233+
self.assertEqual(ts, resp.headers['X-Timestamp'])
229234

230235
def test_valid_sig2(self):
231236
sig = self.sync.realms_conf.get_sig(

test/unit/common/middleware/test_gatekeeper.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,13 @@ class TestGatekeeper(unittest.TestCase):
7474
x_backend_headers = {'X-Backend-Replication': 'true',
7575
'X-Backend-Replication-Headers': 'stuff'}
7676

77+
x_timestamp_headers = {'X-Timestamp': '1455952805.719739'}
78+
7779
forbidden_headers_out = dict(sysmeta_headers.items() +
7880
x_backend_headers.items())
7981
forbidden_headers_in = dict(sysmeta_headers.items() +
8082
x_backend_headers.items())
83+
shunted_headers_in = dict(x_timestamp_headers.items())
8184

8285
def _assertHeadersEqual(self, expected, actual):
8386
for key in expected:
@@ -106,20 +109,63 @@ def test_ok_header(self):
106109
def _test_reserved_header_removed_inbound(self, method):
107110
headers = dict(self.forbidden_headers_in)
108111
headers.update(self.allowed_headers)
112+
headers.update(self.shunted_headers_in)
109113
req = Request.blank('/v/a/c', environ={'REQUEST_METHOD': method},
110114
headers=headers)
111115
fake_app = FakeApp()
112116
app = self.get_app(fake_app, {})
113117
resp = req.get_response(app)
114118
self.assertEqual('200 OK', resp.status)
115-
self._assertHeadersEqual(self.allowed_headers, fake_app.req.headers)
116-
self._assertHeadersAbsent(self.forbidden_headers_in,
117-
fake_app.req.headers)
119+
expected_headers = dict(self.allowed_headers)
120+
# shunt_inbound_x_timestamp should be enabled by default
121+
expected_headers.update({'X-Backend-Inbound-' + k: v
122+
for k, v in self.shunted_headers_in.items()})
123+
self._assertHeadersEqual(expected_headers, fake_app.req.headers)
124+
unexpected_headers = dict(self.forbidden_headers_in.items() +
125+
self.shunted_headers_in.items())
126+
self._assertHeadersAbsent(unexpected_headers, fake_app.req.headers)
118127

119128
def test_reserved_header_removed_inbound(self):
120129
for method in self.methods:
121130
self._test_reserved_header_removed_inbound(method)
122131

132+
def _test_reserved_header_shunted_inbound(self, method):
133+
headers = dict(self.shunted_headers_in)
134+
headers.update(self.allowed_headers)
135+
req = Request.blank('/v/a/c', environ={'REQUEST_METHOD': method},
136+
headers=headers)
137+
fake_app = FakeApp()
138+
app = self.get_app(fake_app, {}, shunt_inbound_x_timestamp='true')
139+
resp = req.get_response(app)
140+
self.assertEqual('200 OK', resp.status)
141+
expected_headers = dict(self.allowed_headers)
142+
expected_headers.update({'X-Backend-Inbound-' + k: v
143+
for k, v in self.shunted_headers_in.items()})
144+
self._assertHeadersEqual(expected_headers, fake_app.req.headers)
145+
self._assertHeadersAbsent(self.shunted_headers_in,
146+
fake_app.req.headers)
147+
148+
def test_reserved_header_shunted_inbound(self):
149+
for method in self.methods:
150+
self._test_reserved_header_shunted_inbound(method)
151+
152+
def _test_reserved_header_shunt_bypassed_inbound(self, method):
153+
headers = dict(self.shunted_headers_in)
154+
headers.update(self.allowed_headers)
155+
req = Request.blank('/v/a/c', environ={'REQUEST_METHOD': method},
156+
headers=headers)
157+
fake_app = FakeApp()
158+
app = self.get_app(fake_app, {}, shunt_inbound_x_timestamp='false')
159+
resp = req.get_response(app)
160+
self.assertEqual('200 OK', resp.status)
161+
expected_headers = dict(self.allowed_headers.items() +
162+
self.shunted_headers_in.items())
163+
self._assertHeadersEqual(expected_headers, fake_app.req.headers)
164+
165+
def test_reserved_header_shunt_bypassed_inbound(self):
166+
for method in self.methods:
167+
self._test_reserved_header_shunt_bypassed_inbound(method)
168+
123169
def _test_reserved_header_removed_outbound(self, method):
124170
headers = dict(self.forbidden_headers_out)
125171
headers.update(self.allowed_headers)

0 commit comments

Comments
 (0)