Skip to content

Commit a8c6fc1

Browse files
authored
Update whitelisting and blacklisting behavior (#53)
To avoid dropping useful header names this updates the SDK to allow headers by default, and that headers are scrubbed instead of dropped if a blacklist or whitelist is applied. Note that User-Agent is always passed, since it is required by the API. Cookie and Authorization are always scrubbed for security reasons.
1 parent 183ac24 commit a8c6fc1

7 files changed

Lines changed: 63 additions & 42 deletions

File tree

README.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import and configure the library with your Castle API secret.
2121

2222
.. code:: python
2323
24-
from castle.configuration import configuration
24+
from castle.configuration import configuration, WHITELISTED
2525
2626
# Same as setting it through Castle.api_secret
2727
configuration.api_secret = ':YOUR-API-SECRET'
@@ -33,15 +33,15 @@ import and configure the library with your Castle API secret.
3333
configuration.request_timeout = 1000
3434
3535
# Whitelisted and Blacklisted headers are case insensitive and allow to use _ and - as a separator, http prefixes are removed
36+
# By default all headers are passed, but some are automatically scrubbed.
37+
# If you need to apply a whitelist, we recommend using the minimum set of
38+
# standard headers that we've exposed in the `WHITELISTED` constant.
3639
# Whitelisted headers
37-
configuration.whitelisted = ['X_HEADER']
38-
# or append to default
39-
configuration.whitelisted = configuration.whitelisted + ['http-x-header']
40+
configuration.whitelisted = WHITELISTED + ['X_HEADER']
4041
41-
# Blacklisted headers take advantage over whitelisted elements
42+
# Blacklisted headers take advantage over whitelisted elements. Note that
43+
# some headers are always scrubbed, for security reasons.
4244
configuration.blacklisted = ['HTTP-X-header']
43-
# or append to default
44-
configuration.blacklisted = configuration.blacklisted + ['X_HEADER']
4545
4646
Tracking
4747
--------

castle/configuration.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,26 @@
22
from castle.headers_formatter import HeadersFormatter
33

44
WHITELISTED = [
5-
'User-Agent',
6-
'Accept-Language',
7-
'Accept-Encoding',
8-
'Accept-Charset',
9-
'Accept',
10-
'Accept-Datetime',
11-
'Forwarded',
12-
'X-Forwarded',
13-
'X-Real-IP',
14-
'REMOTE_ADDR',
15-
'X-Forwarded-For',
16-
'CF_CONNECTING_IP'
5+
"Accept",
6+
"Accept-Charset",
7+
"Accept-Datetime",
8+
"Accept-Encoding",
9+
"Accept-Language",
10+
"Cache-Control",
11+
"Connection",
12+
"Content-Length",
13+
"Content-Type",
14+
"Cookie",
15+
"Host",
16+
"Origin",
17+
"Pragma",
18+
"Referer",
19+
"TE",
20+
"Upgrade-Insecure-Requests",
21+
"User-Agent",
22+
"X-Castle-Client-Id",
1723
]
1824

19-
BLACKLISTED = ['HTTP_COOKIE']
20-
2125
# 500 milliseconds
2226
REQUEST_TIMEOUT = 500
2327
FAILOVER_STRATEGIES = ['allow', 'deny', 'challenge', 'throw']
@@ -29,8 +33,8 @@ def __init__(self):
2933
self.host = 'api.castle.io'
3034
self.port = 443
3135
self.url_prefix = '/v1'
32-
self.whitelisted = WHITELISTED
33-
self.blacklisted = BLACKLISTED
36+
self.whitelisted = []
37+
self.blacklisted = []
3438
self.request_timeout = REQUEST_TIMEOUT
3539
self.failover_strategy = 'allow'
3640

castle/extractors/headers.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from castle.headers_formatter import HeadersFormatter
22
from castle.configuration import configuration
33

4+
DEFAULT_BLACKLIST = ['Cookie', 'Authorization']
5+
DEFAULT_WHITELIST = ['User-Agent']
6+
47

58
class ExtractorsHeaders(object):
69
def __init__(self, environ):
@@ -9,12 +12,15 @@ def __init__(self, environ):
912

1013
def call(self):
1114
headers = dict()
15+
has_whitelist = len(configuration.whitelisted) > 0
1216

1317
for key, value in self.environ.items():
1418
name = self.formatter.call(key)
15-
if name not in configuration.whitelisted:
19+
if has_whitelist and name not in configuration.whitelisted and name not in DEFAULT_WHITELIST:
20+
headers[name] = True
1621
continue
17-
if name in configuration.blacklisted:
22+
if name in configuration.blacklisted or name in DEFAULT_BLACKLIST:
23+
headers[name] = True
1824
continue
1925
headers[name] = value
2026

castle/test/client_test.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ def test_init(self):
3131
context = {
3232
'active': True,
3333
'client_id': '1234',
34-
'headers': {'User-Agent': 'test', 'X-Forwarded-For': '217.144.192.112'},
34+
'headers': {
35+
'User-Agent': 'test',
36+
'X-Forwarded-For': '217.144.192.112',
37+
'X-Castle-Client-Id': '1234'
38+
},
3539
'ip': '217.144.192.112',
3640
'library': {'name': 'castle-python', 'version': VERSION},
3741
'origin': 'web',
@@ -189,7 +193,11 @@ def test_to_context(self):
189193
context = {
190194
'active': True,
191195
'client_id': '1234',
192-
'headers': {'User-Agent': 'test', 'X-Forwarded-For': '217.144.192.112'},
196+
'headers': {
197+
'User-Agent': 'test',
198+
'X-Forwarded-For': '217.144.192.112',
199+
'X-Castle-Client-Id': '1234'
200+
},
193201
'ip': '217.144.192.112',
194202
'library': {'name': 'castle-python', 'version': VERSION},
195203
'origin': 'web',

castle/test/configuration_test.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from castle.test import unittest
22
from castle.exceptions import ConfigurationError
3-
from castle.configuration import Configuration, WHITELISTED, BLACKLISTED
3+
from castle.configuration import Configuration
44
from castle.headers_formatter import HeadersFormatter
55

66

@@ -11,10 +11,8 @@ def test_default_values(self):
1111
self.assertEqual(config.host, 'api.castle.io')
1212
self.assertEqual(config.port, 443)
1313
self.assertEqual(config.url_prefix, '/v1')
14-
self.assertEqual(config.whitelisted, [
15-
HeadersFormatter.call(v) for v in WHITELISTED])
16-
self.assertEqual(config.blacklisted, [
17-
HeadersFormatter.call(v) for v in BLACKLISTED])
14+
self.assertEqual(config.whitelisted, [])
15+
self.assertEqual(config.blacklisted, [])
1816
self.assertEqual(config.request_timeout, 500)
1917
self.assertEqual(config.failover_strategy, 'allow')
2018

castle/test/context/default_test.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def test_default_context(self):
4646
self.assertEqual(context['client_id'], client_id())
4747
self.assertEqual(context['active'], True)
4848
self.assertEqual(context['origin'], 'web')
49-
self.assertEqual(context['headers'], {'X-Forwarded-For': request_ip()})
49+
self.assertEqual(context['headers'], {'X-Forwarded-For': request_ip(), 'Cookie': True})
5050
self.assertEqual(context['ip'], request_ip())
5151
self.assertDictEqual(context['library'], {
5252
'name': 'castle-python', 'version': __version__})
@@ -59,8 +59,12 @@ def test_default_context_with_extras(self):
5959
self.assertEqual(context['origin'], 'web')
6060
self.assertEqual(
6161
context['headers'],
62-
{'X-Forwarded-For': request_ip(), 'Accept-Language': 'en',
63-
'User-Agent': 'test'}
62+
{
63+
'X-Forwarded-For': request_ip(),
64+
'Accept-Language': 'en',
65+
'User-Agent': 'test',
66+
'Cookie': True
67+
}
6468
)
6569
self.assertEqual(context['ip'], request_ip())
6670
self.assertDictEqual(
Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from castle.test import unittest
2-
from castle.configuration import configuration
2+
from castle.configuration import configuration, WHITELISTED
33
from castle.extractors.headers import ExtractorsHeaders
44

55

@@ -9,7 +9,7 @@ def client_id():
99

1010
def environ():
1111
return {
12-
'HTTP_X_FORWARDED_FOR': '1.2.3.4',
12+
'HTTP_USER_AGENT': 'requests',
1313
'HTTP_OK': 'OK',
1414
'TEST': '1',
1515
'HTTP_COOKIE': "__cid={client_id};other=efgh".format(client_id=client_id)
@@ -18,13 +18,14 @@ def environ():
1818

1919
class ExtractorsHeadersTestCase(unittest.TestCase):
2020
def test_extract_headers(self):
21+
configuration.whitelisted = []
2122
self.assertEqual(ExtractorsHeaders(environ()).call(),
22-
{'X-Forwarded-For': '1.2.3.4'})
23+
{'User-Agent': 'requests', 'Ok': 'OK', 'Test': '1', 'Cookie': True})
2324

24-
def test_extend_whitelisted_headers(self):
25-
configuration.whitelisted += ['TEST']
25+
def test_add_whitelisted_headers(self):
26+
configuration.whitelisted = WHITELISTED + ['TEST']
2627
self.assertEqual(
2728
ExtractorsHeaders(environ()).call(),
28-
{'X-Forwarded-For': '1.2.3.4', 'Test': '1'}
29+
{'User-Agent': 'requests', 'Test': '1', 'Cookie': True, 'Ok': True}
2930
)
30-
configuration.whitelisted.remove('Test')
31+
configuration.whitelisted = []

0 commit comments

Comments
 (0)