CVE-2023-46134 (published 2023-10-25T21:15Z)

D-Tale is the combination of a Flask back-end and a React front-end to view & analyze Pandas data structures. Prior to version 3.7.0, users hosting D-Tale publicly can be vulnerable to remote code execution, allowing attackers to run malicious code on the server. This issue has been patched in version 3.7.0 by turning off "Custom Filter" input by default. The only workaround for versions earlier than 3.7.0 is to only host D-Tale to trusted users.\n

CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/man-group/dtale
commit_details: repo_url: https://github.com/man-group/dtale
author: Andrew Schonfeld (Boston)
author_date: 2023-10-02 13:49:59-04:00
msg: Updated 'Custom Filter' feature to be disabled by default
repository: repo_url: https://github.com/man-group/dtale
repo_name: man-group/dtale
description: Visualizer for pandas data structures
repo_language: TypeScript
owner: man-group
file_changes: filename: README.md
change_type: ModificationType.MODIFY
diff: @@ -976,6 +976,20 @@ outliers = s[(s < iqr_lower) | (s > iqr_upper)]
 If you click on the "Apply outlier filter" link this will add an addtional "outlier" filter for this column which can be removed from the [header](#header) or the [custom filter](#custom-filter) shown in picture above to the right.
 
 #### Custom Filter
+
+**Starting with version 3.7.0 this feature will be turned off by default.
+Custom filters are vulnerable to code injection attacks, please only use in trusted environments.**
+
+**You can turn this feature on by doing one of the following:**
+ - **add `enable_custom_filters=True` to your `dtale.show` call**
+ - **add `enable_custom_filters = False` to the [app] section of your dtale.ini config file ([more info](https://github.com/man-group/dtale/blob/master/docs/CONFIGURATION.md))**
+ - **run this code before calling dtale.show:**
+```python
+import dtale.global_state as global_state
+global_state.set_app_settings(dict(enable_custom_filters=True))
+```
+
+
 Apply a custom pandas `query` to your data (link to pandas documentation included in popup)  
 
 |Editing|Result|

programming_language: Markdown
filename: CONFIGURATION.md
change_type: ModificationType.MODIFY
diff: @@ -25,6 +25,7 @@ lock_header_menu = False
 hide_header_menu = False
 hide_main_menu = False
 hide_column_menus = False
+enable_custom_filters = False
 
 [charts] # this controls how many points can be contained within scatter & 3D charts
 scatter_points = 15000

programming_language: Markdown
filename: __init__.py
change_type: ModificationType.MODIFY
diff: @@ -22,6 +22,7 @@ with warnings.catch_warnings():
     HIDE_HEADER_MENU = False
     HIDE_MAIN_MENU = False
     HIDE_COLUMN_MENUS = False
+    ENABLE_CUSTOM_FILTERS = False
 
     # flake8: NOQA
     from dtale.app import show, get_instance, instances, offline_chart  # isort:skip

programming_language: Python
filename: app.py
change_type: ModificationType.MODIFY
diff: @@ -719,6 +719,8 @@ def show(data=None, data_loader=None, name=None, context_vars=None, **options):
     :param highlight_filter: if True, then highlight rows on the frontend which will be filtered when applying a filter
                              rather than hiding them from the dataframe
     :type highlight_filter: boolean, optional
+    :param enable_custom_filters: If true, this will enable users to make custom filters from the UI
+    :type enable_custom_filters: bool, optional
 
     :Example:
 
@@ -804,6 +806,7 @@ def show(data=None, data_loader=None, name=None, context_vars=None, **options):
             hide_header_menu=final_options.get("hide_header_menu"),
             hide_main_menu=final_options.get("hide_main_menu"),
             hide_column_menus=final_options.get("hide_column_menus"),
+            enable_custom_filters=final_options.get("enable_custom_filters"),
         )
         instance.started_with_open_browser = final_options["open_browser"]
         is_active = not running_with_flask_debug() and is_up(app_url)

programming_language: Python
filename: config.py
change_type: ModificationType.MODIFY
diff: @@ -107,6 +107,13 @@ def load_app_settings(config):
         section="app",
         getter="getboolean",
     )
+    enable_custom_filters = get_config_val(
+        config,
+        curr_app_settings,
+        "enable_custom_filters",
+        section="app",
+        getter="getboolean",
+    )
     open_custom_filter_on_startup = get_config_val(
         config,
         curr_app_settings,
@@ -145,6 +152,7 @@ def load_app_settings(config):
             hide_header_menu=hide_header_menu,
             hide_main_menu=hide_main_menu,
             hide_column_menus=hide_column_menus,
+            enable_custom_filters=enable_custom_filters,
         )
     )
 
@@ -214,6 +222,7 @@ def build_show_options(options=None):
         hide_header_menu=None,
         hide_main_menu=None,
         hide_column_menus=None,
+        enable_custom_filters=None,
     )
     config_options = {}
     config = get_config()

programming_language: Python
filename: global_state.py
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,7 @@
 import string
 import inspect
 
-
+from logging import getLogger
 from six import PY3
 
 from dtale.utils import dict_merge, format_data
@@ -11,6 +11,8 @@ try:
 except ImportError:
     from collections import MutableMapping
 
+logger = getLogger(__name__)
+
 APP_SETTINGS = {
     "theme": "light",
     "pin_menu": False,
@@ -30,6 +32,7 @@ APP_SETTINGS = {
     "hide_header_menu": False,
     "hide_main_menu": False,
     "hide_column_menus": False,
+    "enable_custom_filters": False,
 }
 
 AUTH_SETTINGS = {"active": False, "username": None, "password": None}
@@ -602,6 +605,17 @@ def set_app_settings(settings):
         instance_updates["hide_main_menu"] = settings.get("hide_main_menu")
     if settings.get("hide_column_menus") is not None:
         instance_updates["hide_column_menus"] = settings.get("hide_column_menus")
+    if settings.get("enable_custom_filters") is not None:
+        instance_updates["enable_custom_filters"] = settings.get(
+            "enable_custom_filters"
+        )
+        if instance_updates["enable_custom_filters"]:
+            logger.warning(
+                (
+                    "Turning on custom filtering. Custom filters are vulnerable to code injection attacks, please only "
+                    "use in trusted environments."
+                )
+            )
 
     if _default_store.size() > 0 and len(instance_updates):
         for data_id in _default_store.keys():

programming_language: Python
filename: base.html
change_type: ModificationType.MODIFY
diff: @@ -44,6 +44,7 @@
         <input type="hidden" id="hide_header_menu" value="{{hide_header_menu}}" />
         <input type="hidden" id="hide_main_menu" value="{{hide_main_menu}}" />
         <input type="hidden" id="hide_column_menus" value="{{hide_column_menus}}" />
+        <input type="hidden" id="enable_custom_filters" value="{{enable_custom_filters}}" />
         <input type="hidden" id="allow_cell_edits" value="{{allow_cell_edits}}" />
         <input type="hidden" id="hide_drop_rows" value="{{hide_drop_rows}}" />
         <input type="hidden" id="is_vscode" value="{{is_vscode}}" />

programming_language: HTML
filename: views.py
change_type: ModificationType.MODIFY
diff: @@ -351,6 +351,7 @@ class DtaleData(object):
         * hide_header_menu - if true, this will hide the header menu from the screen
         * hide_main_menu - if true, this will hide the main menu from the screen
         * hide_column_menus - if true, this will hide the column menus from the screen
+        * enable_custom_filters - if True, allow users to specify custom filters from the UI using pandas.query strings
 
         After applying please refresh any open browsers!
         """
@@ -906,6 +907,7 @@ def startup(
     hide_header_menu=None,
     hide_main_menu=None,
     hide_column_menus=None,
+    enable_custom_filters=None,
     force_save=True,
 ):
     """
@@ -1033,6 +1035,7 @@ def startup(
             hide_header_menu=hide_header_menu,
             hide_main_menu=hide_main_menu,
             hide_column_menus=hide_column_menus,
+            enable_custom_filters=enable_custom_filters,
         )
         startup_code = (
             "from arcticdb import Arctic\n"
@@ -1104,6 +1107,7 @@ def startup(
                 hide_header_menu=hide_header_menu,
                 hide_main_menu=hide_main_menu,
                 hide_column_menus=hide_column_menus,
+                enable_custom_filters=enable_custom_filters,
             )
 
             global_state.set_dataset(instance._data_id, data)
@@ -1171,6 +1175,8 @@ def startup(
             base_settings["hide_main_menu"] = hide_main_menu
         if hide_column_menus is not None:
             base_settings["hide_column_menus"] = hide_column_menus
+        if enable_custom_filters is not None:
+            base_settings["enable_custom_filters"] = enable_custom_filters
         if column_edit_options is not None:
             base_settings["column_edit_options"] = column_edit_options
         global_state.set_settings(data_id, base_settings)
@@ -1218,6 +1224,13 @@ def startup(
         global_state.set_context_variables(
             data_id, build_context_variables(data_id, context_vars)
         )
+        if global_state.load_flag(data_id, "enable_custom_filters", False):
+            logger.warning(
+                (
+                    "Custom filtering enabled. Custom filters are vulnerable to code injection attacks, please only "
+                    "use in trusted environments."
+                )
+            )
         return DtaleData(data_id, url, is_proxy=is_proxy, app_root=app_root)
     else:
         raise NoDataLoadedException("No data has been loaded into this D-Tale session!")
@@ -1251,6 +1264,9 @@ def base_render_template(template, data_id, **kwargs):
     hide_header_menu = global_state.load_flag(data_id, "hide_header_menu", False)
     hide_main_menu = global_state.load_flag(data_id, "hide_main_menu", False)
     hide_column_menus = global_state.load_flag(data_id, "hide_column_menus", False)
+    enable_custom_filters = global_state.load_flag(
+        data_id, "enable_custom_filters", False
+    )
     app_overrides = dict(
         allow_cell_edits=json.dumps(allow_cell_edits),
         hide_shutdown=hide_shutdown,
@@ -1259,6 +1275,7 @@ def base_render_template(template, data_id, **kwargs):
         hide_header_menu=hide_header_menu,
         hide_main_menu=hide_main_menu,
         hide_column_menus=hide_column_menus,
+        enable_custom_filters=enable_custom_filters,
         github_fork=github_fork,
     )
     is_arcticdb = 0
@@ -1996,6 +2013,16 @@ def test_filter(data_id):
     :return: JSON {success: True/False}
     """
     query = get_str_arg(request, "query")
+    if query and not global_state.load_flag(data_id, "enable_custom_filters", False):
+        return jsonify(
+            dict(
+                success=False,
+                error=(
+                    "Custom Filters not enabled! Custom filters are vulnerable to code injection attacks, please only "
+                    "use in trusted environments."
+                ),
+            )
+        )
     run_query(
         handle_predefined(data_id),
         build_query(data_id, query),

programming_language: Python
filename: DataViewer-filter-test.tsx
change_type: ModificationType.MODIFY
diff: @@ -5,6 +5,7 @@ import { Provider } from 'react-redux';
 import { Store } from 'redux';
 
 import { DataViewer } from '../../dtale/DataViewer';
+import { DISABLED_CUSTOM_FILTERS_MSG } from '../../popups/filter/FilterPopup';
 import DimensionsHelper from '../DimensionsHelper';
 import reduxUtils from '../redux-test-utils';
 import { buildInnerHTML, clickMainMenuButton, mockChartJS } from '../test-utils';
@@ -46,9 +47,9 @@ describe('FilterPanel', () => {
     await clickMainMenuButton('Custom Filter');
   };
 
-  const buildResult = async (dataId = '1'): Promise<void> => {
+  const buildResult = async (dataId = '1', overrides?: Record<string, string>): Promise<void> => {
     store = reduxUtils.createDtaleStore();
-    buildInnerHTML({ settings: '', dataId }, store);
+    buildInnerHTML({ settings: '', dataId, enableCustomFilters: 'True', ...overrides }, store);
     await act(() => {
       const result = render(
         <Provider store={store}>
@@ -135,4 +136,14 @@ describe('FilterPanel', () => {
     });
     expect(screen.queryByTestId('structured-filters')).toBeNull();
   });
+
+  it('DataViewer: filtering with custom filtering not enabled', async () => {
+    await buildResult('1', { enableCustomFilters: 'False' });
+    const textarea = screen.getByTestId('filter-panel').getElementsByTagName('textarea')[0];
+    expect(textarea.value).toBe(DISABLED_CUSTOM_FILTERS_MSG);
+    expect(textarea).toBeDisabled();
+    const buttons = [...screen.getByTestId('filter-panel').querySelectorAll('button')];
+    expect(buttons.filter((b) => b.textContent === 'Apply')).toHaveLength(0);
+    expect(buttons.filter((b) => b.textContent === 'Clear')).toHaveLength(0);
+  });
 });

programming_language: TypeScript
filename: reduxGridUtils-test.ts
change_type: ModificationType.MODIFY
diff: @@ -23,6 +23,7 @@ describe('reduxGridUtils', () => {
       hide_header_menu: false,
       hide_main_menu: false,
       hide_column_menus: false,
+      enable_custom_filters: false,
     };
     reduxUtils.handleReduxState(
       columns,

programming_language: TypeScript
filename: FilterPopup-test.tsx
change_type: ModificationType.MODIFY
diff: @@ -40,7 +40,7 @@ describe('FilterPopup', () => {
     updateQueryEngineSpy = jest.spyOn(serverState, 'updateQueryEngine');
     updateQueryEngineSpy.mockResolvedValue(Promise.resolve({ success: true }));
     store = reduxUtils.createDtaleStore();
-    buildInnerHTML({ settings: '', dataId: '1', queryEngine: 'python' }, store);
+    buildInnerHTML({ settings: '', dataId: '1', queryEngine: 'python', enableCustomFilters: 'True' }, store);
     store.dispatch({ type: ActionType.OPEN_CHART, chartData: { visible: true } });
     wrapper = await act(
       async () =>

programming_language: TypeScript
filename: dtale-test.tsx
change_type: ModificationType.MODIFY
diff: @@ -28,6 +28,7 @@ describe('reducer tests', () => {
       hideHeaderMenu: false,
       hideMainMenu: false,
       hideColumnMenus: false,
+      enableCustomFilters: false,
       hideDropRows: false,
       iframe: false,
       columnMenuOpen: false,
@@ -52,6 +53,7 @@ describe('reducer tests', () => {
         hide_header_menu: false,
         hide_main_menu: false,
         hide_column_menus: false,
+        enable_custom_filters: false,
       },
       pythonVersion: null,
       isPreview: false,

programming_language: TypeScript
filename: test-utils.tsx
change_type: ModificationType.MODIFY
diff: @@ -95,6 +95,7 @@ export const buildInnerHTML = (props: Record<string, string | undefined> = {}, s
     buildHidden('hide_header_menu', props.hideHeaderMenu ?? HIDE_SHUTDOWN),
     buildHidden('hide_main_menu', props.hideMainMenu ?? HIDE_SHUTDOWN),
     buildHidden('hide_column_menus', props.hideColumnMenus ?? HIDE_SHUTDOWN),
+    buildHidden('enable_custom_filters', props.enableCustomFilters ?? HIDE_SHUTDOWN),
     BASE_HTML,
   ].join('');
   store?.dispatch(actions.init());

programming_language: TypeScript
filename: main.tsx
change_type: ModificationType.MODIFY
diff: @@ -23,6 +23,7 @@ actions.loadLockHeaderMenu(store);
 actions.loadHideHeaderMenu(store);
 actions.loadHideMainMenu(store);
 actions.loadHideColumnMenus(store);
+actions.loadEnableCustomFilters(store);
 const root = ReactDOMClient.createRoot(document.getElementById('content')!);
 root.render(
   <Provider store={store}>

programming_language: TypeScript
filename: main.tsx
change_type: ModificationType.MODIFY
diff: @@ -48,6 +48,7 @@ let storeBuilder: () => Store = () => {
   actions.loadHideHeaderMenu(store);
   actions.loadHideMainMenu(store);
   actions.loadHideColumnMenus(store);
+  actions.loadEnableCustomFilters(store);
   return store;
 };
 if (pathname.indexOf('/dtale/popup') === 0) {

programming_language: TypeScript
filename: FilterPanel.tsx
change_type: ModificationType.MODIFY
diff: @@ -1,4 +1,3 @@
-import { createSelector } from '@reduxjs/toolkit';
 import * as React from 'react';
 import { WithTranslation, withTranslation } from 'react-i18next';
 import { useDispatch, useSelector } from 'react-redux';
@@ -11,24 +10,19 @@ import SidePanelButtons from '../../dtale/side/SidePanelButtons';
 import { ActionType, HideSidePanelAction, SetQueryEngineAction } from '../../redux/actions/AppActions';
 import * as dtaleActions from '../../redux/actions/dtale';
 import * as settingsActions from '../../redux/actions/settings';
-import { selectDataId, selectQueryEngine, selectSettings } from '../../redux/selectors';
 import { InstanceSettings, QueryEngine } from '../../redux/state/AppState';
 import { RemovableError } from '../../RemovableError';
 import * as CustomFilterRepository from '../../repository/CustomFilterRepository';
 import { Checkbox } from '../create/LabeledCheckbox';
 
 import ContextVariables from './ContextVariables';
+import { DISABLED_CUSTOM_FILTERS_MSG, selectResult } from './FilterPopup';
 import PandasQueryHelp from './PandasQueryHelp';
 import QueryExamples from './QueryExamples';
 import StructuredFilters from './StructuredFilters';
 
-const selectResult = createSelector(
-  [selectDataId, selectQueryEngine, selectSettings],
-  (dataId, queryEngine, settings) => ({ dataId, queryEngine, settings }),
-);
-
 const FilterPanel: React.FC<WithTranslation> = ({ t }) => {
-  const { dataId, queryEngine, settings } = useSelector(selectResult);
+  const { dataId, enableCustomFilters, queryEngine, settings } = useSelector(selectResult);
   const dispatch = useDispatch();
   const hideSidePanel = (): HideSidePanelAction => dispatch({ type: ActionType.HIDE_SIDE_PANEL });
   const updateSettings = (updatedSettings: Partial<InstanceSettings>, callback?: () => void): AnyAction =>
@@ -109,17 +103,22 @@ const FilterPanel: React.FC<WithTranslation> = ({ t }) => {
           <div className="row m-0 pb-3">
             <div className="col p-0 font-weight-bold mt-auto">{t('Custom Filter', { ns: 'filter' })}</div>
             <PandasQueryHelp />
-            <button className="btn btn-primary col-auto pt-2 pb-2" onClick={clear}>
-              <span>{t('Clear', { ns: 'filter' })}</span>
-            </button>
-            <button className="btn btn-primary col-auto pt-2 pb-2" onClick={save}>
-              <span>{t('Apply', { ns: 'filter' })}</span>
-            </button>
+            {enableCustomFilters && (
+              <>
+                <button className="btn btn-primary col-auto pt-2 pb-2" onClick={clear}>
+                  <span>{t('Clear', { ns: 'filter' })}</span>
+                </button>
+                <button className="btn btn-primary col-auto pt-2 pb-2" onClick={save}>
+                  <span>{t('Apply', { ns: 'filter' })}</span>
+                </button>
+              </>
+            )}
           </div>
           <textarea
             style={{ width: '100%', height: 150 }}
-            value={query || ''}
+            value={enableCustomFilters ? query : DISABLED_CUSTOM_FILTERS_MSG}
             onChange={(event) => setQuery(event.target.value)}
+            disabled={!enableCustomFilters}
           />
         </div>
       </div>

programming_language: TypeScript
filename: FilterPopup.tsx
change_type: ModificationType.MODIFY
diff: @@ -11,7 +11,7 @@ import { CloseChartAction, SetQueryEngineAction } from '../../redux/actions/AppA
 import { closeChart } from '../../redux/actions/charts';
 import * as dtaleActions from '../../redux/actions/dtale';
 import * as settingsActions from '../../redux/actions/settings';
-import { selectDataId, selectQueryEngine, selectSettings } from '../../redux/selectors';
+import { selectDataId, selectEnableCustomFilters, selectQueryEngine, selectSettings } from '../../redux/selectors';
 import { InstanceSettings, QueryEngine } from '../../redux/state/AppState';
 import { RemovableError } from '../../RemovableError';
 import * as CustomFilterRepository from '../../repository/CustomFilterRepository';
@@ -22,13 +22,22 @@ import PandasQueryHelp from './PandasQueryHelp';
 import QueryExamples from './QueryExamples';
 import StructuredFilters from './StructuredFilters';
 
-const selectResult = createSelector(
-  [selectDataId, selectQueryEngine, selectSettings],
-  (dataId, queryEngine, settings) => ({ dataId, queryEngine, settings }),
+export const DISABLED_CUSTOM_FILTERS_MSG = [
+  'Custom Filtering is currently disabled.  This feature is only for trusted environments, in order to unlock this ',
+  'feature you must do one of the following:\n\n',
+  '- add "enable_custom_filters=True" to your dtale.show call\n',
+  '- run this code before calling dtale.show\n',
+  '\timport dtale.global_state as global_state\n\tglobal_state.set_app_settings(dict(enable_custom_filters=True))\n',
+  '- add "enable_custom_filters = False" to the [app] section of your dtale.ini config file',
+].join('');
+
+export const selectResult = createSelector(
+  [selectDataId, selectQueryEngine, selectEnableCustomFilters, selectSettings],
+  (dataId, queryEngine, enableCustomFilters, settings) => ({ dataId, queryEngine, enableCustomFilters, settings }),
 );
 
 const FilterPopup: React.FC<WithTranslation> = ({ t }) => {
-  const { dataId, queryEngine, settings } = useSelector(selectResult);
+  const { dataId, queryEngine, enableCustomFilters, settings } = useSelector(selectResult);
   const dispatch = useDispatch();
   const onClose = (): CloseChartAction => dispatch(closeChart());
   const updateSettings = (updatedSettings: Partial<InstanceSettings>, callback?: () => void): AnyAction =>
@@ -140,8 +149,9 @@ const FilterPopup: React.FC<WithTranslation> = ({ t }) => {
                 <div className="font-weight-bold pt-3 pb-3">{t('Custom Filter', { ns: 'filter' })}</div>
                 <textarea
                   style={{ width: '100%', height: 150 }}
-                  value={query || ''}
+                  value={enableCustomFilters ? query : DISABLED_CUSTOM_FILTERS_MSG}
                   onChange={(event) => setQuery(event.target.value)}
+                  disabled={!enableCustomFilters}
                 />
               </div>
             </div>
@@ -165,12 +175,16 @@ const FilterPopup: React.FC<WithTranslation> = ({ t }) => {
       </div>
       <div className="modal-footer">
         <PandasQueryHelp />
-        <button className="btn btn-primary" onClick={clear}>
-          <span>{t('Clear', { ns: 'filter' })}</span>
-        </button>
-        <button className="btn btn-primary" onClick={save}>
-          <span>{t('Apply', { ns: 'filter' })}</span>
-        </button>
+        {enableCustomFilters && (
+          <>
+            <button className="btn btn-primary" onClick={clear}>
+              <span>{t('Clear', { ns: 'filter' })}</span>
+            </button>
+            <button className="btn btn-primary" onClick={save}>
+              <span>{t('Apply', { ns: 'filter' })}</span>
+            </button>
+          </>
+        )}
       </div>
     </React.Fragment>
   );

programming_language: TypeScript
filename: AppActions.ts
change_type: ModificationType.MODIFY
diff: @@ -63,6 +63,7 @@ export enum ActionType {
   UPDATE_HIDE_HEADER_MENU = 'update-hide-header-menu',
   UPDATE_HIDE_MAIN_MENU = 'update-hide-main-menu',
   UPDATE_HIDE_COLUMN_MENUS = 'update-hide-column-menus',
+  UPDATE_ENABLE_CUSTOM_FILTERS = 'update-enable-custom-filters',
 }
 
 /** Action fired when a range is selected */
@@ -257,6 +258,11 @@ export interface UpdateHideColumnMenus extends Action<typeof ActionType.UPDATE_H
   value: boolean;
 }
 
+/** Action fired when updating the enable_custom_filters flag */
+export interface UpdateEnableCustomFilters extends Action<typeof ActionType.UPDATE_ENABLE_CUSTOM_FILTERS> {
+  value: boolean;
+}
+
 /** Type definition encompassing all application actions */
 export type AppActionTypes =
   | InitAction
@@ -302,7 +308,8 @@ export type AppActionTypes =
   | UpdateLockHeaderMenu
   | UpdateHideHeaderMenu
   | UpdateHideMainMenu
-  | UpdateHideColumnMenus;
+  | UpdateHideColumnMenus
+  | UpdateEnableCustomFilters;
 
 /** Type definition for redux application actions */
 export type AppActions<R> = ThunkAction<R, AppState, Record<string, unknown>, AnyAction>;

programming_language: TypeScript
filename: dtale.ts
change_type: ModificationType.MODIFY
diff: @@ -77,6 +77,14 @@ export const loadHideColumnMenus = (store: Store<AppState, AnyAction>): void =>
   });
 };
 
+export const loadEnableCustomFilters = (store: Store<AppState, AnyAction>): void => {
+  const { settings, enableCustomFilters } = store.getState();
+  store.dispatch({
+    type: ActionType.UPDATE_ENABLE_CUSTOM_FILTERS,
+    value: enableCustomFilters ?? settings.enable_custom_filters ?? enableCustomFilters,
+  });
+};
+
 export const openCustomFilter = (): SidePanelAction => ({
   type: ActionType.SHOW_SIDE_PANEL,
   view: SidePanelType.FILTER,

programming_language: TypeScript
filename: settings.ts
change_type: ModificationType.MODIFY
diff: @@ -80,6 +80,19 @@ export const hideColumnMenus = (state = false, action: AppActionTypes): boolean
   }
 };
 
+export const enableCustomFilters = (state = false, action: AppActionTypes): boolean => {
+  switch (action.type) {
+    case ActionType.INIT_PARAMS:
+      return toBool(getHiddenValue('enable_custom_filters'));
+    case ActionType.UPDATE_ENABLE_CUSTOM_FILTERS:
+      return action.value;
+    case ActionType.LOAD_PREVIEW:
+      return false;
+    default:
+      return state;
+  }
+};
+
 export const openCustomFilterOnStartup = (state = false, action: AppActionTypes): boolean => {
   switch (action.type) {
     case ActionType.INIT_PARAMS:

programming_language: TypeScript
filename: selectors.ts
change_type: ModificationType.MODIFY
diff: @@ -58,6 +58,7 @@ export const selectBaseLockHeaderMenu = (state: AppState): boolean => state.lock
 export const selectBaseHideHeaderMenu = (state: AppState): boolean => state.hideHeaderMenu;
 export const selectBaseHideMainMenu = (state: AppState): boolean => state.hideMainMenu;
 export const selectBaseHideColumnMenus = (state: AppState): boolean => state.hideColumnMenus;
+export const selectBaseEnableCustomFilters = (state: AppState): boolean => state.enableCustomFilters;
 export const selectFilteredRanges = (state: AppState): FilteredRanges => state.filteredRanges;
 export const selectShowAllHeatmapColumns = (state: AppState): boolean => state.showAllHeatmapColumns;
 export const selectChartData = (state: AppState): Popups => state.chartData;
@@ -106,6 +107,14 @@ export const selectHideColumnMenus = createSelector(
   [selectSettingsHideColumnMenus, selectBaseHideColumnMenus],
   (settingsHideColumnMenus, hideColumnMenus) => settingsHideColumnMenus ?? hideColumnMenus,
 );
+const selectSettingsEnableCustomFilters = createSelector(
+  [selectSettings],
+  (settings) => settings?.enable_custom_filters,
+);
+export const selectEnableCustomFilters = createSelector(
+  [selectSettingsEnableCustomFilters, selectBaseEnableCustomFilters],
+  (settingsEnableCustomFilters, enableCustomFilters) => settingsEnableCustomFilters ?? enableCustomFilters,
+);
 export const selectRibbonMenuOpen = createSelector(
   [selectBaseRibbonMenuOpen, selectLockHeaderMenu, selectHideHeaderMenu],
   (ribbonMenuOpen, lockHeaderMenu, hideHeaderMenu) => (ribbonMenuOpen || lockHeaderMenu) && !hideHeaderMenu,

programming_language: TypeScript
filename: AppState.ts
change_type: ModificationType.MODIFY
diff: @@ -382,6 +382,7 @@ export interface InstanceSettings {
   hide_main_menu: boolean;
   hide_column_menus: boolean;
   isArcticDB?: number;
+  enable_custom_filters: boolean;
 }
 
 export const BASE_INSTANCE_SETTINGS: InstanceSettings = Object.freeze({
@@ -395,6 +396,7 @@ export const BASE_INSTANCE_SETTINGS: InstanceSettings = Object.freeze({
   hide_header_menu: false,
   hide_main_menu: false,
   hide_column_menus: false,
+  enable_custom_filters: false,
 });
 
 /** Type definition for semantic versioning of python */
@@ -437,6 +439,7 @@ export interface AppSettings {
   hideHeaderMenu: boolean;
   hideMainMenu: boolean;
   hideColumnMenus: boolean;
+  enableCustomFilters: boolean;
 }
 
 /** Properties for specifying filtered ranges */

programming_language: TypeScript
filename: dtale.ini
change_type: ModificationType.MODIFY
diff: @@ -13,6 +13,7 @@ lock_header_menu = False
 hide_header_menu = False
 hide_main_menu = False
 hide_column_menus = False
+enable_custom_filters = False
 
 [charts]
 scatter_points = 15000

programming_language: Haskell
filename: dtale_missing_props.ini
change_type: ModificationType.MODIFY
diff: @@ -5,3 +5,4 @@ lock_header_menu = False
 hide_header_menu = False
 hide_main_menu = False
 hide_column_menus = False
+enable_custom_filters = False

programming_language: Ruby
filename: test_config.py
change_type: ModificationType.MODIFY
diff: @@ -27,6 +27,7 @@ def test_load_app_settings():
         "hide_header_menu": True,
         "hide_main_menu": True,
         "hide_column_menus": True,
+        "enable_custom_filters": True,
     }
     with ExitStack() as stack:
         stack.enter_context(mock.patch("dtale.global_state.APP_SETTINGS", settings))
@@ -43,6 +44,7 @@ def test_load_app_settings():
         assert settings["hide_header_menu"]
         assert settings["hide_main_menu"]
         assert settings["hide_column_menus"]
+        assert settings["enable_custom_filters"]
 
         load_app_settings(
             load_config_state(os.path.join(os.path.dirname(__file__), "dtale.ini"))
@@ -60,6 +62,7 @@ def test_load_app_settings():
         assert not settings["hide_header_menu"]
         assert not settings["hide_main_menu"]
         assert not settings["hide_column_menus"]
+        assert not settings["enable_custom_filters"]
 
 
 @pytest.mark.unit
@@ -77,6 +80,7 @@ def test_load_app_settings_w_missing_props():
         "hide_header_menu": True,
         "hide_main_menu": True,
         "hide_column_menus": True,
+        "enable_custom_filters": True,
     }
     with ExitStack() as stack:
         stack.enter_context(mock.patch("dtale.global_state.APP_SETTINGS", settings))
@@ -91,6 +95,7 @@ def test_load_app_settings_w_missing_props():
         assert settings["hide_header_menu"]
         assert settings["hide_main_menu"]
         assert settings["hide_column_menus"]
+        assert settings["enable_custom_filters"]
 
         load_app_settings(
             load_config_state(
@@ -106,6 +111,7 @@ def test_load_app_settings_w_missing_props():
         assert not settings["hide_header_menu"]
         assert not settings["hide_main_menu"]
         assert not settings["hide_column_menus"]
+        assert not settings["enable_custom_filters"]
 
 
 @pytest.mark.unit

programming_language: Python
filename: test_views.py
change_type: ModificationType.MODIFY
diff: @@ -25,6 +25,14 @@ URL = "http://localhost:40000"
 app = build_app(url=URL)
 
 
+def setup_function(function):
+    global_state.cleanup()
+
+
+def teardown_function(function):
+    global_state.cleanup()
+
+
 @pytest.mark.unit
 def test_head_endpoint():
     import dtale.views as views
@@ -72,6 +80,7 @@ def test_startup(unittest):
         hide_header_menu=True,
         hide_main_menu=True,
         hide_column_menus=True,
+        enable_custom_filters=True,
     )
 
     pdt.assert_frame_equal(instance.data, test_data.reset_index())
@@ -86,6 +95,7 @@ def test_startup(unittest):
             hide_header_menu=True,
             hide_main_menu=True,
             hide_column_menus=True,
+            enable_custom_filters=True,
             locked=["date", "security_id"],
             indexes=["date", "security_id"],
             precision=2,
@@ -110,6 +120,7 @@ def test_startup(unittest):
             hide_header_menu=True,
             hide_main_menu=True,
             hide_column_menus=True,
+            enable_custom_filters=True,
             locked=["date", "security_id"],
             indexes=["date", "security_id"],
             precision=2,
@@ -1329,6 +1340,7 @@ def test_variance(unittest):
 def test_test_filter(test_data):
     with app.test_client() as c:
         build_data_inst({c.port: test_data})
+        global_state.set_app_settings(dict(enable_custom_filters=True))
         response = c.get(
             "/dtale/test-filter/{}".format(c.port),
             query_string=dict(query="date == date"),
@@ -1375,6 +1387,20 @@ def test_test_filter(test_data):
         )
         response_data = response.get_json()
         assert response_data["success"]
+
+        global_state.set_app_settings(dict(enable_custom_filters=False))
+        response = c.get(
+            "/dtale/test-filter/{}".format(c.port),
+            query_string=dict(query="foo2 == 1", save=True),
+        )
+        response_data = response.get_json()
+        assert not response_data["success"]
+        assert response_data["error"] == (
+            "Custom Filters not enabled! Custom filters are vulnerable to code injection attacks, please only "
+            "use in trusted environments."
+        )
+        global_state.set_app_settings(dict(enable_custom_filters=True))
+
     if PY3:
         df = pd.DataFrame([dict(a=1)])
         df["a.b"] = 2

programming_language: Python