Skip to content

Commit 954f6be

Browse files
committed
Updated web data uploads to be turned off by default
1 parent 063423a commit 954f6be

27 files changed

+259
-35
lines changed

.circleci/config.yml

+4
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ python: &python
155155
. ci/bin/activate
156156
# xlrd & xarray do not load correctly from "python setup.py develop"
157157
# possibly switch this script to use "pip install -r requirements.txt"
158+
pip install pillow
158159
pip install xlrd
159160
pip install xarray
160161
if [ "${CIRCLE_JOB}" == "build_3_10" ]; then
@@ -191,6 +192,9 @@ python: &python
191192
command: |
192193
set -e
193194
. ci/bin/activate
195+
if [ "${CIRCLE_JOB}" == "build_2_7" ]; then
196+
pip install backports.functools-lru-cache==1.6.6
197+
fi
194198
if [ "${CIRCLE_JOB}" != "build_2_7" ]; then
195199
pip install -e ".[arcticdb]"
196200
fi

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -1352,6 +1352,19 @@ Here's the options at you disposal:
13521352
* pandas.util.testing.makeTimeDataFrame
13531353

13541354

1355+
**Starting with version 3.8.1 web uploads will be turned off by default.
1356+
Web uploads are vulnerable to blind server side request forgery, please only use in trusted environments.**
1357+
1358+
**You can turn this feature on by doing one of the following:**
1359+
- **add `enable_web_uploads=True` to your `dtale.show` call**
1360+
- **add `enable_web_uploads = False` to the [app] section of your dtale.ini config file ([more info](https://github.com/man-group/dtale/blob/master/docs/CONFIGURATION.md))**
1361+
- **run this code before calling dtale.show:**
1362+
```python
1363+
import dtale.global_state as global_state
1364+
global_state.set_app_settings(dict(enable_web_uploads=True))
1365+
```
1366+
1367+
13551368
#### Instances
13561369
This will give you information about other D-Tale instances are running under your current Python process.
13571370

docs/CONFIGURATION.md

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ hide_header_menu = False
2626
hide_main_menu = False
2727
hide_column_menus = False
2828
enable_custom_filters = False
29+
enable_web_uploads = False
2930

3031
[charts] # this controls how many points can be contained within scatter & 3D charts
3132
scatter_points = 15000

dtale/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
HIDE_MAIN_MENU = False
2424
HIDE_COLUMN_MENUS = False
2525
ENABLE_CUSTOM_FILTERS = False
26+
ENABLE_WEB_UPLOADS = False
2627

2728
# flake8: NOQA
2829
from dtale.app import show, get_instance, instances, offline_chart # isort:skip

dtale/app.py

+3
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,8 @@ def show(data=None, data_loader=None, name=None, context_vars=None, **options):
721721
:type highlight_filter: boolean, optional
722722
:param enable_custom_filters: If true, this will enable users to make custom filters from the UI
723723
:type enable_custom_filters: bool, optional
724+
:param enable_web_uploads: If true, this will enable users to upload files using URLs from the UI
725+
:type enable_web_uploads: bool, optional
724726
725727
:Example:
726728
@@ -807,6 +809,7 @@ def show(data=None, data_loader=None, name=None, context_vars=None, **options):
807809
hide_main_menu=final_options.get("hide_main_menu"),
808810
hide_column_menus=final_options.get("hide_column_menus"),
809811
enable_custom_filters=final_options.get("enable_custom_filters"),
812+
enable_web_uploads=final_options.get("enable_web_uploads"),
810813
)
811814
instance.started_with_open_browser = final_options["open_browser"]
812815
is_active = not running_with_flask_debug() and is_up(app_url)

dtale/config.py

+9
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ def load_app_settings(config):
114114
section="app",
115115
getter="getboolean",
116116
)
117+
enable_web_uploads = get_config_val(
118+
config,
119+
curr_app_settings,
120+
"enable_web_uploads",
121+
section="app",
122+
getter="getboolean",
123+
)
117124
open_custom_filter_on_startup = get_config_val(
118125
config,
119126
curr_app_settings,
@@ -153,6 +160,7 @@ def load_app_settings(config):
153160
hide_main_menu=hide_main_menu,
154161
hide_column_menus=hide_column_menus,
155162
enable_custom_filters=enable_custom_filters,
163+
enable_web_uploads=enable_web_uploads,
156164
)
157165
)
158166

@@ -223,6 +231,7 @@ def build_show_options(options=None):
223231
hide_main_menu=None,
224232
hide_column_menus=None,
225233
enable_custom_filters=None,
234+
enable_web_uploads=None,
226235
)
227236
config_options = {}
228237
config = get_config()

dtale/datasets.py

+19-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import numpy as np
12
import pandas as pd
23
import requests
4+
import string
35
import zipfile
6+
from datetime import datetime
47

58
from six import BytesIO
69

@@ -89,11 +92,19 @@ def movies():
8992

9093

9194
def time_dataframe():
92-
try:
93-
from pandas._testing import makeTimeDataFrame
94-
95-
return makeTimeDataFrame(), None
96-
except ImportError:
97-
from pandas.util.testing import makeTimeDataFrame
98-
99-
return makeTimeDataFrame(), None
95+
def series_data():
96+
if hasattr(np.random, "default_rng"):
97+
return np.random.default_rng(2).standard_normal(30)
98+
return np.random.randn(30)
99+
100+
cols = string.ascii_uppercase[:4]
101+
data = {
102+
c: pd.Series(
103+
series_data(),
104+
index=pd.DatetimeIndex(
105+
pd.date_range(datetime(2000, 1, 1), periods=30, freq="B")
106+
),
107+
)
108+
for c in cols
109+
}
110+
return pd.DataFrame(data), None

dtale/global_state.py

+10
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"hide_main_menu": False,
3434
"hide_column_menus": False,
3535
"enable_custom_filters": False,
36+
"enable_web_uploads": False,
3637
}
3738

3839
AUTH_SETTINGS = {"active": False, "username": None, "password": None}
@@ -616,6 +617,15 @@ def set_app_settings(settings):
616617
"use in trusted environments."
617618
)
618619
)
620+
if settings.get("enable_web_uploads") is not None:
621+
instance_updates["enable_web_uploads"] = settings.get("enable_web_uploads")
622+
if instance_updates["enable_web_uploads"]:
623+
logger.warning(
624+
(
625+
"Turning on Web uploads. Web uploads are vulnerable to blind server side request forgery, please "
626+
"only use in trusted environments."
627+
)
628+
)
619629

620630
if _default_store.size() > 0 and len(instance_updates):
621631
for data_id in _default_store.keys():

dtale/templates/dtale/base.html

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
<input type="hidden" id="hide_main_menu" value="{{hide_main_menu}}" />
4646
<input type="hidden" id="hide_column_menus" value="{{hide_column_menus}}" />
4747
<input type="hidden" id="enable_custom_filters" value="{{enable_custom_filters}}" />
48+
<input type="hidden" id="enable_web_uploads" value="{{enable_web_uploads}}" />
4849
<input type="hidden" id="allow_cell_edits" value="{{allow_cell_edits}}" />
4950
<input type="hidden" id="hide_drop_rows" value="{{hide_drop_rows}}" />
5051
<input type="hidden" id="is_vscode" value="{{is_vscode}}" />

dtale/views.py

+26
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ def update_settings(self, **updates):
352352
* hide_main_menu - if true, this will hide the main menu from the screen
353353
* hide_column_menus - if true, this will hide the column menus from the screen
354354
* enable_custom_filters - if True, allow users to specify custom filters from the UI using pandas.query strings
355+
* enable_web_uploads - if True, allow users to upload files using URLs from the UI
355356
356357
After applying please refresh any open browsers!
357358
"""
@@ -916,6 +917,7 @@ def startup(
916917
hide_main_menu=None,
917918
hide_column_menus=None,
918919
enable_custom_filters=None,
920+
enable_web_uploads=None,
919921
force_save=True,
920922
):
921923
"""
@@ -1044,6 +1046,7 @@ def startup(
10441046
hide_main_menu=hide_main_menu,
10451047
hide_column_menus=hide_column_menus,
10461048
enable_custom_filters=enable_custom_filters,
1049+
enable_web_uploads=enable_web_uploads,
10471050
)
10481051
startup_code = (
10491052
"from arcticdb import Arctic\n"
@@ -1116,6 +1119,7 @@ def startup(
11161119
hide_main_menu=hide_main_menu,
11171120
hide_column_menus=hide_column_menus,
11181121
enable_custom_filters=enable_custom_filters,
1122+
enable_web_uploads=enable_web_uploads,
11191123
)
11201124

11211125
global_state.set_dataset(instance._data_id, data)
@@ -1185,6 +1189,8 @@ def startup(
11851189
base_settings["hide_column_menus"] = hide_column_menus
11861190
if enable_custom_filters is not None:
11871191
base_settings["enable_custom_filters"] = enable_custom_filters
1192+
if enable_web_uploads is not None:
1193+
base_settings["enable_web_uploads"] = enable_web_uploads
11881194
if column_edit_options is not None:
11891195
base_settings["column_edit_options"] = column_edit_options
11901196
global_state.set_settings(data_id, base_settings)
@@ -1239,6 +1245,13 @@ def startup(
12391245
"use in trusted environments."
12401246
)
12411247
)
1248+
if global_state.load_flag(data_id, "enable_web_uploads", False):
1249+
logger.warning(
1250+
(
1251+
"Web uploads enabled. Web uploads are vulnerable to blind server side request forgery, please "
1252+
"only use in trusted environments."
1253+
)
1254+
)
12421255
return DtaleData(data_id, url, is_proxy=is_proxy, app_root=app_root)
12431256
else:
12441257
raise NoDataLoadedException("No data has been loaded into this D-Tale session!")
@@ -1275,6 +1288,7 @@ def base_render_template(template, data_id, **kwargs):
12751288
enable_custom_filters = global_state.load_flag(
12761289
data_id, "enable_custom_filters", False
12771290
)
1291+
enable_web_uploads = global_state.load_flag(data_id, "enable_web_uploads", False)
12781292
app_overrides = dict(
12791293
allow_cell_edits=json.dumps(allow_cell_edits),
12801294
hide_shutdown=hide_shutdown,
@@ -1284,6 +1298,7 @@ def base_render_template(template, data_id, **kwargs):
12841298
hide_main_menu=hide_main_menu,
12851299
hide_column_menus=hide_column_menus,
12861300
enable_custom_filters=enable_custom_filters,
1301+
enable_web_uploads=enable_web_uploads,
12871302
github_fork=github_fork,
12881303
)
12891304
is_arcticdb = 0
@@ -3926,6 +3941,17 @@ def web_upload():
39263941
from dtale.cli.loaders.excel_loader import load_file as load_excel
39273942
from dtale.cli.loaders.parquet_loader import loader_func as load_parquet
39283943

3944+
if not global_state.get_app_settings().get("enable_web_uploads", False):
3945+
return jsonify(
3946+
dict(
3947+
success=False,
3948+
error=(
3949+
"Web uploads not enabled! Web uploads are vulnerable to blind server side request forgery, please "
3950+
"only use in trusted environments."
3951+
),
3952+
)
3953+
)
3954+
39293955
data_type = get_str_arg(request, "type")
39303956
url = get_str_arg(request, "url")
39313957
proxy = get_str_arg(request, "proxy")

frontend/static/__tests__/dtale/upload/Upload.test.support.tsx

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { act, render } from '@testing-library/react';
22
import axios from 'axios';
33
import * as React from 'react';
4+
import { Provider } from 'react-redux';
5+
import { Store } from 'redux';
46

57
import Upload from '../../../popups/upload/Upload';
68
import { Dataset, DataType } from '../../../popups/upload/UploadState';
79
import * as UploadRepository from '../../../repository/UploadRepository';
810
import reduxUtils from '../../redux-test-utils';
11+
import { buildInnerHTML } from '../../test-utils';
912

1013
/** Bundles alot of jest setup for CreateColumn component tests */
1114
export class Spies {
@@ -17,6 +20,7 @@ export class Spies {
1720
public presetUploadSpy: jest.SpyInstance<Promise<UploadRepository.UploadResponse | undefined>, [dataset: Dataset]>;
1821
public readAsDataURLSpy: jest.SpyInstance;
1922
public btoaSpy: jest.SpyInstance;
23+
public store: Store;
2024

2125
/** Initializes all spy instances */
2226
constructor() {
@@ -25,6 +29,7 @@ export class Spies {
2529
this.presetUploadSpy = jest.spyOn(UploadRepository, 'presetUpload');
2630
this.readAsDataURLSpy = jest.spyOn(FileReader.prototype, 'readAsDataURL');
2731
this.btoaSpy = jest.spyOn(window, 'btoa');
32+
this.store = reduxUtils.createDtaleStore();
2833
}
2934

3035
/** Sets the mockImplementation/mockReturnValue for spy instances */
@@ -48,11 +53,21 @@ export class Spies {
4853
/**
4954
* Build the initial wrapper.
5055
*
56+
* @param overrides redux overrides
5157
* @return the wrapper for testing.
5258
*/
53-
public async setupWrapper(): Promise<Element> {
59+
public async setupWrapper(overrides?: Record<string, string>): Promise<Element> {
60+
this.store = reduxUtils.createDtaleStore();
61+
buildInnerHTML({ enableWebUploads: 'True', ...overrides }, this.store);
5462
return await act((): Element => {
55-
const { container } = render(<Upload />);
63+
const { container } = render(
64+
<Provider store={this.store}>
65+
<Upload />
66+
</Provider>,
67+
{
68+
container: document.getElementById('content') ?? undefined,
69+
},
70+
);
5671
return container;
5772
});
5873
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { screen } from '@testing-library/react';
2+
3+
import { DISABLED_URL_UPLOADS_MSG } from '../../../popups/upload/Upload';
4+
5+
import * as TestSupport from './Upload.test.support';
6+
7+
describe('Upload', () => {
8+
const { close, location, open, opener } = window;
9+
const spies = new TestSupport.Spies();
10+
11+
beforeEach(async () => {
12+
delete (window as any).location;
13+
delete (window as any).close;
14+
delete (window as any).open;
15+
delete window.opener;
16+
(window as any).location = {
17+
reload: jest.fn(),
18+
pathname: '/dtale/column/1',
19+
href: '',
20+
assign: jest.fn(),
21+
};
22+
window.close = jest.fn();
23+
window.open = jest.fn();
24+
window.opener = { location: { assign: jest.fn(), pathname: '/dtale/column/1' } };
25+
spies.setupMockImplementations();
26+
await spies.setupWrapper({ enableWebUploads: 'False' });
27+
});
28+
29+
afterEach(() => spies.afterEach());
30+
31+
afterAll(() => {
32+
spies.afterAll();
33+
window.location = location;
34+
window.close = close;
35+
window.open = open;
36+
window.opener = opener;
37+
});
38+
39+
const upload = (): HTMLElement => screen.getByTestId('upload');
40+
41+
it('renders successfully', async () => {
42+
expect(upload()).toBeDefined();
43+
});
44+
45+
it('DataViewer: disabled web uploads', async () => {
46+
expect(upload().getElementsByClassName('form-group')[0].textContent).toBe(DISABLED_URL_UPLOADS_MSG);
47+
});
48+
});

frontend/static/__tests__/reducers/dtale-test.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ describe('reducer tests', () => {
2929
hideMainMenu: false,
3030
hideColumnMenus: false,
3131
enableCustomFilters: false,
32+
enableWebUploads: false,
3233
hideDropRows: false,
3334
iframe: false,
3435
columnMenuOpen: false,

frontend/static/__tests__/test-utils.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export const buildInnerHTML = (props: Record<string, string | undefined> = {}, s
9696
buildHidden('hide_main_menu', props.hideMainMenu ?? HIDE_SHUTDOWN),
9797
buildHidden('hide_column_menus', props.hideColumnMenus ?? HIDE_SHUTDOWN),
9898
buildHidden('enable_custom_filters', props.enableCustomFilters ?? HIDE_SHUTDOWN),
99+
buildHidden('enable_web_uploads', props.enableWebUploads ?? HIDE_SHUTDOWN),
99100
BASE_HTML,
100101
].join('');
101102
store?.dispatch(actions.init());

frontend/static/popups/create/LabeledInput.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ interface DtaleInputProps {
55
type?: React.HTMLInputTypeAttribute;
66
value?: any;
77
setter: (value: string) => void;
8-
inputOptions?: Partial<React.HTMLAttributes<HTMLInputElement>>;
8+
inputOptions?: Partial<React.AllHTMLAttributes<HTMLInputElement>>;
99
}
1010

1111
const DtaleInput: React.FC<DtaleInputProps> = ({ type = 'text', value, setter, inputOptions }) => (

frontend/static/popups/filter/FilterPopup.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const DISABLED_CUSTOM_FILTERS_MSG = [
2828
'- add "enable_custom_filters=True" to your dtale.show call\n',
2929
'- run this code before calling dtale.show\n',
3030
'\timport dtale.global_state as global_state\n\tglobal_state.set_app_settings(dict(enable_custom_filters=True))\n',
31-
'- add "enable_custom_filters = False" to the [app] section of your dtale.ini config file',
31+
'- add "enable_custom_filters = True" to the [app] section of your dtale.ini config file',
3232
].join('');
3333

3434
export const selectResult = createSelector(

0 commit comments

Comments
 (0)