CVE-2023-46134: Insufficient Information (NVD-CWE-noinfo)
value: 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.

CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 9.8
Published: 2023-10-25T21:15Z
Repository: https://github.com/man-group/dtale
Language: Other
Files changed: README.md, docs/CONFIGURATION.md, dtale/__init__.py, dtale/app.py, dtale/config.py, dtale/global_state.py, dtale/templates/dtale/base.html, dtale/views.py, frontend/static/__tests__/dtale/DataViewer-filter-test.tsx, frontend/static/__tests__/dtale/reduxGridUtils-test.ts, frontend/static/__tests__/popups/filter/FilterPopup-test.tsx, frontend/static/__tests__/reducers/dtale-test.tsx, frontend/static/__tests__/test-utils.tsx, frontend/static/dtale/export/main.tsx, frontend/static/main.tsx, frontend/static/popups/filter/FilterPanel.tsx, frontend/static/popups/filter/FilterPopup.tsx, frontend/static/redux/actions/AppActions.ts, frontend/static/redux/actions/dtale.ts, frontend/static/redux/reducers/app/settings.ts, frontend/static/redux/selectors.ts, frontend/static/redux/state/AppState.ts, tests/dtale/config/dtale.ini, tests/dtale/config/dtale_missing_props.ini, tests/dtale/config/test_config.py, tests/dtale/test_views.py
Commit date: 2023-10-02 15:00:43 -0400
Commit message: Updated 'Custom Filter' feature to be disabled by default

Diff: diff --git a/README.md b/README.md
index 5caf4f96..680ff02d 100644
--- a/README.md
+++ b/README.md
@@ -974,10 +974,24 @@ iqr_upper = q3 + 1.5 * iqr
 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|
 |--------|:------:|
 |![](https://raw.githubusercontent.com/aschonfeld/dtale-media/master/images/Filter_apply.png)|![](https://raw.githubusercontent.com/aschonfeld/dtale-media/master/images/Post_filter.png)|
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index bf32a05f..6fbf4e1f 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -23,10 +23,11 @@ query_engine = python
 hide_header_editor = False
 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
 3d_points = 4000
 
diff --git a/dtale/__init__.py b/dtale/__init__.py
index 6e0e690d..3020b424 100644
--- a/dtale/__init__.py
+++ b/dtale/__init__.py
@@ -20,10 +20,11 @@ with warnings.catch_warnings():
     HIDE_HEADER_EDITOR = False
     LOCK_HEADER_MENU = False
     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
     from dtale.cli.loaders import LOADERS  # isort:skip
     from dtale.cli.clickutils import retrieve_meta_info_and_version
diff --git a/dtale/app.py b/dtale/app.py
index 46a6196d..b4222a89 100644
--- a/dtale/app.py
+++ b/dtale/app.py
@@ -717,10 +717,12 @@ def show(data=None, data_loader=None, name=None, context_vars=None, **options):
                                     NaN values
     :type auto_hide_empty_columns: boolean, optional
     :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:
 
         >>> import dtale
         >>> import pandas as pd
@@ -802,10 +804,11 @@ def show(data=None, data_loader=None, name=None, context_vars=None, **options):
             hide_header_editor=final_options.get("hide_header_editor"),
             lock_header_menu=final_options.get("lock_header_menu"),
             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)
         if is_active:
 
diff --git a/dtale/config.py b/dtale/config.py
index 1edd30e4..6e09727b 100644
--- a/dtale/config.py
+++ b/dtale/config.py
@@ -105,10 +105,17 @@ def load_app_settings(config):
         curr_app_settings,
         "lock_header_menu",
         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,
         "open_custom_filter_on_startup",
         section="app",
@@ -143,10 +150,11 @@ def load_app_settings(config):
             hide_header_editor=hide_header_editor,
             lock_header_menu=lock_header_menu,
             hide_header_menu=hide_header_menu,
             hide_main_menu=hide_main_menu,
             hide_column_menus=hide_column_menus,
+            enable_custom_filters=enable_custom_filters,
         )
     )
 
 
 def load_chart_settings(config):
@@ -212,10 +220,11 @@ def build_show_options(options=None):
         hide_header_editor=None,
         lock_header_menu=None,
         hide_header_menu=None,
         hide_main_menu=None,
         hide_column_menus=None,
+        enable_custom_filters=None,
     )
     config_options = {}
     config = get_config()
     if config and config.has_section("show"):
         config_options["host"] = get_config_val(config, defaults, "host")
diff --git a/dtale/global_state.py b/dtale/global_state.py
index df99392e..58684226 100644
--- a/dtale/global_state.py
+++ b/dtale/global_state.py
@@ -1,18 +1,20 @@
 import string
 import inspect
 
-
+from logging import getLogger
 from six import PY3
 
 from dtale.utils import dict_merge, format_data
 
 try:
     from collections.abc import MutableMapping
 except ImportError:
     from collections import MutableMapping
 
+logger = getLogger(__name__)
+
 APP_SETTINGS = {
     "theme": "light",
     "pin_menu": False,
     "language": "en",
     "github_fork": False,
@@ -28,10 +30,11 @@ APP_SETTINGS = {
     "hide_header_editor": False,
     "lock_header_menu": False,
     "hide_header_menu": False,
     "hide_main_menu": False,
     "hide_column_menus": False,
+    "enable_custom_filters": False,
 }
 
 AUTH_SETTINGS = {"active": False, "username": None, "password": None}
 
 CHART_SETTINGS = {"scatter_points": 15000, "3d_points": 40000}
@@ -600,10 +603,21 @@ def set_app_settings(settings):
         instance_updates["hide_header_menu"] = settings.get("hide_header_menu")
     if settings.get("hide_main_menu") is not None:
         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():
             update_settings(data_id, instance_updates)
 
diff --git a/dtale/templates/dtale/base.html b/dtale/templates/dtale/base.html
index b71cf484..d85d3f94 100644
--- a/dtale/templates/dtale/base.html
+++ b/dtale/templates/dtale/base.html
@@ -42,10 +42,11 @@
         <input type="hidden" id="hide_header_editor" value="{{hide_header_editor}}" />
         <input type="hidden" id="lock_header_menu" value="{{lock_header_menu}}" />
         <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}}" />
         <input type="hidden" id="is_arcticdb" value="{{is_arcticdb}}" />
         <input type="hidden" id="arctic_conn" value="{{arctic_conn}}" />
diff --git a/dtale/views.py b/dtale/views.py
index 61f57b49..4c6260f3 100644
--- a/dtale/views.py
+++ b/dtale/views.py
@@ -349,10 +349,11 @@ class DtaleData(object):
         * lock_header_menu - if true, this will always the display the header menu which usually only displays when you
                              hover over the top
         * 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!
         """
         name_updates = dict(
             range_highlights="rangeHighlight",
@@ -904,10 +905,11 @@ def startup(
     hide_header_editor=None,
     lock_header_menu=None,
     hide_header_menu=None,
     hide_main_menu=None,
     hide_column_menus=None,
+    enable_custom_filters=None,
     force_save=True,
 ):
     """
     Loads and stores data globally
      - If data has indexes then it will lock save those columns as locked on the front-end
@@ -1031,10 +1033,11 @@ def startup(
             hide_header_editor=hide_header_editor,
             lock_header_menu=lock_header_menu,
             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"
             "from arcticdb.version_store._store import VersionedItem\n\n"
             "conn = Arctic('{uri}')\n"
@@ -1102,10 +1105,11 @@ def startup(
                 hide_header_editor=hide_header_editor,
                 lock_header_menu=lock_header_menu,
                 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)
             global_state.set_dataset_dim(instance._data_id, {})
             return instance
@@ -1169,10 +1173,12 @@ def startup(
             base_settings["hide_header_menu"] = hide_header_menu
         if hide_main_menu is not None:
             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)
         if optimize_dataframe and not global_state.is_arcticdb:
             data = optimize_df(data)
@@ -1216,10 +1222,17 @@ def startup(
                     col["visible"] = False
         global_state.set_dtypes(data_id, dtypes_state)
         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!")
 
 
@@ -1249,18 +1262,22 @@ def base_render_template(template, data_id, **kwargs):
     hide_header_editor = global_state.load_flag(data_id, "hide_header_editor", False)
     lock_header_menu = global_state.load_flag(data_id, "lock_header_menu", False)
     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,
         hide_header_editor=hide_header_editor,
         lock_header_menu=lock_header_menu,
         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
     arctic_conn = ""
     if global_state.is_arcticdb:
@@ -1994,10 +2011,20 @@ def test_filter(data_id):
     :type data_id: str
     :param query: string from flask.request.args['query'] which is applied to DATA using the query() function
     :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),
         global_state.get_context_variables(data_id),
     )
diff --git a/frontend/static/__tests__/dtale/DataViewer-filter-test.tsx b/frontend/static/__tests__/dtale/DataViewer-filter-test.tsx
index a9e4bee1..1e9586ed 100644
--- a/frontend/static/__tests__/dtale/DataViewer-filter-test.tsx
+++ b/frontend/static/__tests__/dtale/DataViewer-filter-test.tsx
@@ -3,10 +3,11 @@ import axios from 'axios';
 import * as React from 'react';
 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';
 
 describe('FilterPanel', () => {
@@ -44,13 +45,13 @@ describe('FilterPanel', () => {
 
   const toggleFilterMenu = async (): Promise<void> => {
     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}>
           <DataViewer />
         </Provider>,
@@ -133,6 +134,16 @@ describe('FilterPanel', () => {
     await act(async () => {
       fireEvent.click(screen.getByTestId('structured-filters').getElementsByClassName('ico-cancel')[0]);
     });
     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);
+  });
 });
diff --git a/frontend/static/__tests__/dtale/reduxGridUtils-test.ts b/frontend/static/__tests__/dtale/reduxGridUtils-test.ts
index ac98a00f..0deb3fb1 100644
--- a/frontend/static/__tests__/dtale/reduxGridUtils-test.ts
+++ b/frontend/static/__tests__/dtale/reduxGridUtils-test.ts
@@ -21,10 +21,11 @@ describe('reduxGridUtils', () => {
       hide_header_editor: false,
       lock_header_menu: false,
       hide_header_menu: false,
       hide_main_menu: false,
       hide_column_menus: false,
+      enable_custom_filters: false,
     };
     reduxUtils.handleReduxState(
       columns,
       {},
       1,
diff --git a/frontend/static/__tests__/popups/filter/FilterPopup-test.tsx b/frontend/static/__tests__/popups/filter/FilterPopup-test.tsx
index d67c6676..aef0b79d 100644
--- a/frontend/static/__tests__/popups/filter/FilterPopup-test.tsx
+++ b/frontend/static/__tests__/popups/filter/FilterPopup-test.tsx
@@ -38,11 +38,11 @@ describe('FilterPopup', () => {
     updateSettingsSpy = jest.spyOn(serverState, 'updateSettings');
     updateSettingsSpy.mockResolvedValue(Promise.resolve({ success: true }));
     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 () =>
         await render(
           <Provider store={store}>
diff --git a/frontend/static/__tests__/reducers/dtale-test.tsx b/frontend/static/__tests__/reducers/dtale-test.tsx
index 351a7684..72f8cf9e 100644
--- a/frontend/static/__tests__/reducers/dtale-test.tsx
+++ b/frontend/static/__tests__/reducers/dtale-test.tsx
@@ -26,10 +26,11 @@ describe('reducer tests', () => {
       hideHeaderEditor: false,
       lockHeaderMenu: false,
       hideHeaderMenu: false,
       hideMainMenu: false,
       hideColumnMenus: false,
+      enableCustomFilters: false,
       hideDropRows: false,
       iframe: false,
       columnMenuOpen: false,
       selectedCol: null,
       selectedColRef: null,
@@ -50,10 +51,11 @@ describe('reducer tests', () => {
         hide_header_editor: false,
         lock_header_menu: false,
         hide_header_menu: false,
         hide_main_menu: false,
         hide_column_menus: false,
+        enable_custom_filters: false,
       },
       pythonVersion: null,
       isPreview: false,
       menuPinned: false,
       menuTooltip: {
diff --git a/frontend/static/__tests__/test-utils.tsx b/frontend/static/__tests__/test-utils.tsx
index 0e66f008..518d5b78 100644
--- a/frontend/static/__tests__/test-utils.tsx
+++ b/frontend/static/__tests__/test-utils.tsx
@@ -93,10 +93,11 @@ export const buildInnerHTML = (props: Record<string, string | undefined> = {}, s
     buildHidden('hide_header_editor', props.hideHeaderEditor ?? HIDE_SHUTDOWN),
     buildHidden('lock_header_menu', props.lockHeaderMenu ?? HIDE_SHUTDOWN),
     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());
 };
 
diff --git a/frontend/static/dtale/export/main.tsx b/frontend/static/dtale/export/main.tsx
index 9a586c94..be722567 100644
--- a/frontend/static/dtale/export/main.tsx
+++ b/frontend/static/dtale/export/main.tsx
@@ -21,10 +21,11 @@ actions.loadAllowCellEdits(store);
 actions.loadHideHeaderEditor(store);
 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}>
     <ServerlessDataViewer response={(global as any).RESPONSE! as DataResponseContent} />
   </Provider>,
diff --git a/frontend/static/main.tsx b/frontend/static/main.tsx
index 6252d2db..0031540a 100644
--- a/frontend/static/main.tsx
+++ b/frontend/static/main.tsx
@@ -46,10 +46,11 @@ let storeBuilder: () => Store = () => {
   actions.loadHideHeaderEditor(store);
   actions.loadLockHeaderMenu(store);
   actions.loadHideHeaderMenu(store);
   actions.loadHideMainMenu(store);
   actions.loadHideColumnMenus(store);
+  actions.loadEnableCustomFilters(store);
   return store;
 };
 if (pathname.indexOf('/dtale/popup') === 0) {
   require('./dtale/DataViewer.css');
 
diff --git a/frontend/static/popups/filter/FilterPanel.tsx b/frontend/static/popups/filter/FilterPanel.tsx
index 0775d2f5..cdd0adc5 100644
--- a/frontend/static/popups/filter/FilterPanel.tsx
+++ b/frontend/static/popups/filter/FilterPanel.tsx
@@ -1,6 +1,5 @@
-import { createSelector } from '@reduxjs/toolkit';
 import * as React from 'react';
 import { WithTranslation, withTranslation } from 'react-i18next';
 import { useDispatch, useSelector } from 'react-redux';
 import { AnyAction } from 'redux';
 
@@ -9,28 +8,23 @@ import { ColumnFilter, OutlierFilter } from '../../dtale/DataViewerState';
 import * as serverState from '../../dtale/serverStateManagement';
 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 =>
     dispatch(settingsActions.updateSettings(updatedSettings, callback) as any as AnyAction);
   const setEngine = (engine: QueryEngine): SetQueryEngineAction => dispatch(dtaleActions.setQueryEngine(engine));
@@ -107,21 +101,26 @@ const FilterPanel: React.FC<WithTranslation> = ({ t }) => {
             <SidePanelButtons />
           </div>
           <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>
       <div className="row pt-3 pb-3">
         <span className="font-weight-bold col-auto pr-3">{t('Highlight Filtered Rows', { ns: 'main' })}</span>
diff --git a/frontend/static/popups/filter/FilterPopup.tsx b/frontend/static/popups/filter/FilterPopup.tsx
index 4c82c05b..7009b30e 100644
--- a/frontend/static/popups/filter/FilterPopup.tsx
+++ b/frontend/static/popups/filter/FilterPopup.tsx
@@ -9,28 +9,37 @@ import { ColumnFilter, OutlierFilter } from '../../dtale/DataViewerState';
 import * as serverState from '../../dtale/serverStateManagement';
 import { CloseChartAction, SetQueryEngineAction } from '../../redux/actions/AppActions';
 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';
 import { Checkbox } from '../create/LabeledCheckbox';
 
 import ContextVariables from './ContextVariables';
 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 =>
     dispatch(settingsActions.updateSettings(updatedSettings, callback) as any as AnyAction);
   const setEngine = (engine: QueryEngine): SetQueryEngineAction => dispatch(dtaleActions.setQueryEngine(engine));
@@ -138,12 +147,13 @@ const FilterPopup: React.FC<WithTranslation> = ({ t }) => {
                   dropFilter={(col: string) => dropFilter('outlierFilters', outlierFilters, setOutlierFilters, col)}
                 />
                 <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>
           </div>
           <div className="col-md-5">
@@ -163,16 +173,20 @@ const FilterPopup: React.FC<WithTranslation> = ({ t }) => {
           <div className="col-md-12">{contextVars.length > 0 && <ContextVariables contextVars={contextVars} />}</div>
         </div>
       </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>
   );
 };
 
diff --git a/frontend/static/redux/actions/AppActions.ts b/frontend/static/redux/actions/AppActions.ts
index 87ce0014..9a33dd54 100644
--- a/frontend/static/redux/actions/AppActions.ts
+++ b/frontend/static/redux/actions/AppActions.ts
@@ -61,10 +61,11 @@ export enum ActionType {
   UPDATE_HIDE_HEADER_EDITOR = 'update-hide-header-editor',
   UPDATE_LOCK_HEADER_MENU = 'update-lock-header-menu',
   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 */
 export type SetRangeStateAction = Action<typeof ActionType.SET_RANGE_STATE> & RangeState;
 
@@ -255,10 +256,15 @@ export interface UpdateHideMainMenu extends Action<typeof ActionType.UPDATE_HIDE
 /** Action fired when updating the hide_column_menus flag */
 export interface UpdateHideColumnMenus extends Action<typeof ActionType.UPDATE_HIDE_COLUMN_MENUS> {
   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
   | ClearEditAction
   | ConvertToXarrayAction
@@ -300,9 +306,10 @@ export type AppActionTypes =
   | UpdateAllowCellEdits
   | UpdateHideHeaderEditor
   | UpdateLockHeaderMenu
   | UpdateHideHeaderMenu
   | UpdateHideMainMenu
-  | UpdateHideColumnMenus;
+  | UpdateHideColumnMenus
+  | UpdateEnableCustomFilters;
 
 /** Type definition for redux application actions */
 export type AppActions<R> = ThunkAction<R, AppState, Record<string, unknown>, AnyAction>;
diff --git a/frontend/static/redux/actions/dtale.ts b/frontend/static/redux/actions/dtale.ts
index 3437b64d..836c853f 100644
--- a/frontend/static/redux/actions/dtale.ts
+++ b/frontend/static/redux/actions/dtale.ts
@@ -75,10 +75,18 @@ export const loadHideColumnMenus = (store: Store<AppState, AnyAction>): void =>
     type: ActionType.UPDATE_HIDE_COLUMN_MENUS,
     value: hideColumnMenus ?? settings.hide_column_menus ?? hideColumnMenus,
   });
 };
 
+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,
 });
 
diff --git a/frontend/static/redux/reducers/app/settings.ts b/frontend/static/redux/reducers/app/settings.ts
index d082bca0..4a01d118 100644
--- a/frontend/static/redux/reducers/app/settings.ts
+++ b/frontend/static/redux/reducers/app/settings.ts
@@ -78,10 +78,23 @@ export const hideColumnMenus = (state = false, action: AppActionTypes): boolean
     default:
       return state;
   }
 };
 
+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:
       return toBool(getHiddenValue('open_custom_filter_on_startup'));
     case ActionType.LOAD_PREVIEW:
diff --git a/frontend/static/redux/selectors.ts b/frontend/static/redux/selectors.ts
index e31d98bd..497eb755 100644
--- a/frontend/static/redux/selectors.ts
+++ b/frontend/static/redux/selectors.ts
@@ -56,10 +56,11 @@ export const selectBackgroundMode = createSelector([selectSettings], (settings)
 export const selectRangeHighlight = createSelector([selectSettings], (settings) => settings?.rangeHighlight);
 export const selectBaseLockHeaderMenu = (state: AppState): boolean => state.lockHeaderMenu;
 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;
 export const selectSidePanel = (state: AppState): SidePanelProps => state.sidePanel;
 export const selectSidePanelVisible = createSelector([selectSidePanel], (sidePanel) => sidePanel.visible);
@@ -104,10 +105,18 @@ export const selectHideMainMenu = createSelector(
 const selectSettingsHideColumnMenus = createSelector([selectSettings], (settings) => settings?.hide_column_menus);
 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,
 );
 export const selectMainTitle = (state: AppState): string | null => state.mainTitle;
diff --git a/frontend/static/redux/state/AppState.ts b/frontend/static/redux/state/AppState.ts
index 7d0bf6bf..8f10e445 100644
--- a/frontend/static/redux/state/AppState.ts
+++ b/frontend/static/redux/state/AppState.ts
@@ -380,10 +380,11 @@ export interface InstanceSettings {
   lock_header_menu: boolean;
   hide_header_menu: boolean;
   hide_main_menu: boolean;
   hide_column_menus: boolean;
   isArcticDB?: number;
+  enable_custom_filters: boolean;
 }
 
 export const BASE_INSTANCE_SETTINGS: InstanceSettings = Object.freeze({
   allow_cell_edits: true,
   hide_shutdown: false,
@@ -393,10 +394,11 @@ export const BASE_INSTANCE_SETTINGS: InstanceSettings = Object.freeze({
   hide_header_editor: false,
   lock_header_menu: false,
   hide_header_menu: false,
   hide_main_menu: false,
   hide_column_menus: false,
+  enable_custom_filters: false,
 });
 
 /** Type definition for semantic versioning of python */
 export type Version = [number, number, number];
 
@@ -435,10 +437,11 @@ export interface AppSettings {
   hideHeaderEditor: boolean;
   lockHeaderMenu: boolean;
   hideHeaderMenu: boolean;
   hideMainMenu: boolean;
   hideColumnMenus: boolean;
+  enableCustomFilters: boolean;
 }
 
 /** Properties for specifying filtered ranges */
 export interface FilteredRanges {
   query?: string;
diff --git a/tests/dtale/config/dtale.ini b/tests/dtale/config/dtale.ini
index a3aeda3f..52fa121f 100644
--- a/tests/dtale/config/dtale.ini
+++ b/tests/dtale/config/dtale.ini
@@ -11,10 +11,11 @@ query_engine = python
 hide_header_editor = False
 lock_header_menu = False
 hide_header_menu = False
 hide_main_menu = False
 hide_column_menus = False
+enable_custom_filters = False
 
 [charts]
 scatter_points = 15000
 3d_points = 40000
 
diff --git a/tests/dtale/config/dtale_missing_props.ini b/tests/dtale/config/dtale_missing_props.ini
index 891ebba4..9d2c9343 100644
--- a/tests/dtale/config/dtale_missing_props.ini
+++ b/tests/dtale/config/dtale_missing_props.ini
@@ -3,5 +3,6 @@ hide_shutdown = False
 hide_header_editor = False
 lock_header_menu = False
 hide_header_menu = False
 hide_main_menu = False
 hide_column_menus = False
+enable_custom_filters = False
diff --git a/tests/dtale/config/test_config.py b/tests/dtale/config/test_config.py
index a9b85fd7..e8c04970 100644
--- a/tests/dtale/config/test_config.py
+++ b/tests/dtale/config/test_config.py
@@ -25,10 +25,11 @@ def test_load_app_settings():
         "hide_header_editor": True,
         "lock_header_menu": True,
         "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))
 
         load_app_settings(None)
@@ -41,10 +42,11 @@ def test_load_app_settings():
         assert settings["hide_header_editor"]
         assert settings["lock_header_menu"]
         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"))
         )
         assert not settings["hide_shutdown"]
@@ -58,10 +60,11 @@ def test_load_app_settings():
         assert not settings["hide_header_editor"]
         assert not settings["lock_header_menu"]
         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
 def test_load_app_settings_w_missing_props():
     settings = {
@@ -75,10 +78,11 @@ def test_load_app_settings_w_missing_props():
         "hide_header_editor": True,
         "lock_header_menu": True,
         "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))
 
         load_app_settings(None)
@@ -89,10 +93,11 @@ def test_load_app_settings_w_missing_props():
         assert settings["hide_header_editor"]
         assert settings["lock_header_menu"]
         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_missing_props.ini")
             )
@@ -104,10 +109,11 @@ def test_load_app_settings_w_missing_props():
         assert not settings["hide_header_editor"]
         assert not settings["lock_header_menu"]
         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
 def test_load_auth_settings():
     settings = {"active": True, "username": "foo", "password": "foo"}
diff --git a/tests/dtale/test_views.py b/tests/dtale/test_views.py
index 83aa1236..b0258627 100644
--- a/tests/dtale/test_views.py
+++ b/tests/dtale/test_views.py
@@ -23,10 +23,18 @@ from tests.dtale.test_charts import build_col_def
 
 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
 
     build_data_inst({"1": None, "2": None})
@@ -70,10 +78,11 @@ def test_startup(unittest):
         hide_shutdown=True,
         lock_header_menu=True,
         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())
     unittest.assertEqual(
         global_state.get_settings(instance._data_id),
@@ -84,10 +93,11 @@ def test_startup(unittest):
             hide_header_editor=True,
             lock_header_menu=True,
             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,
             sortInfo=[("security_id", "ASC")],
             rangeHighlight=None,
@@ -108,10 +118,11 @@ def test_startup(unittest):
             hide_header_editor=False,
             lock_header_menu=True,
             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,
             sortInfo=[("security_id", "ASC")],
             rangeHighlight=None,
@@ -1327,10 +1338,11 @@ def test_variance(unittest):
 
 @pytest.mark.unit
 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"),
         )
         response_data = response.get_json()
@@ -1373,10 +1385,24 @@ def test_test_filter(test_data):
             "/dtale/test-filter/{}".format(c.port),
             query_string=dict(query=None, save="true"),
         )
         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
         with app.test_client() as c:
             build_data_inst({c.port: df})

Vulnerable code: 
  const buildResult = async (dataId = '1'): Promise<void> => {
    buildInnerHTML({ settings: '', dataId }, store);
    buildInnerHTML({ settings: '', dataId: '1', queryEngine: 'python' }, store);
import { createSelector } from '@reduxjs/toolkit';
import { selectDataId, selectQueryEngine, selectSettings } from '../../redux/selectors';
const selectResult = createSelector(
  [selectDataId, selectQueryEngine, selectSettings],
  (dataId, queryEngine, settings) => ({ dataId, queryEngine, settings }),
);

  const { dataId, queryEngine, settings } = useSelector(selectResult);
            <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>
            value={query || ''}
import { selectDataId, selectQueryEngine, selectSettings } from '../../redux/selectors';
const selectResult = createSelector(
  [selectDataId, selectQueryEngine, selectSettings],
  (dataId, queryEngine, settings) => ({ dataId, queryEngine, settings }),
  const { dataId, queryEngine, settings } = useSelector(selectResult);
                  value={query || ''}
        <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>
  | UpdateHideColumnMenus;
Fixed code: 
**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))
```


enable_custom_filters = False
    ENABLE_CUSTOM_FILTERS = False
    :param enable_custom_filters: If true, this will enable users to make custom filters from the UI
    :type enable_custom_filters: bool, optional
            enable_custom_filters=final_options.get("enable_custom_filters"),
    enable_custom_filters = get_config_val(
        config,
        curr_app_settings,
        "enable_custom_filters",
        section="app",
        getter="getboolean",
    )
            enable_custom_filters=enable_custom_filters,
        enable_custom_filters=None,
from logging import getLogger
logger = getLogger(__name__)

    "enable_custom_filters": False,
    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."
                )
            )
        <input type="hidden" id="enable_custom_filters" value="{{enable_custom_filters}}" />
        * enable_custom_filters - if True, allow users to specify custom filters from the UI using pandas.query strings
    enable_custom_filters=None,
            enable_custom_filters=enable_custom_filters,
                enable_custom_filters=enable_custom_filters,
        if enable_custom_filters is not None:
            base_settings["enable_custom_filters"] = enable_custom_filters
        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."
                )
            )
    enable_custom_filters = global_state.load_flag(
        data_id, "enable_custom_filters", False
    )
        enable_custom_filters=enable_custom_filters,
    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."
                ),
            )
        )
import { DISABLED_CUSTOM_FILTERS_MSG } from '../../popups/filter/FilterPopup';
  const buildResult = async (dataId = '1', overrides?: Record<string, string>): Promise<void> => {
    buildInnerHTML({ settings: '', dataId, enableCustomFilters: 'True', ...overrides }, store);

  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);
  });
      enable_custom_filters: false,
    buildInnerHTML({ settings: '', dataId: '1', queryEngine: 'python', enableCustomFilters: 'True' }, store);
      enableCustomFilters: false,
        enable_custom_filters: false,
    buildHidden('enable_custom_filters', props.enableCustomFilters ?? HIDE_SHUTDOWN),
actions.loadEnableCustomFilters(store);
  actions.loadEnableCustomFilters(store);
import { DISABLED_CUSTOM_FILTERS_MSG, selectResult } from './FilterPopup';
  const { dataId, enableCustomFilters, queryEngine, settings } = useSelector(selectResult);
            {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>
              </>
            )}
            value={enableCustomFilters ? query : DISABLED_CUSTOM_FILTERS_MSG}
            disabled={!enableCustomFilters}
import { selectDataId, selectEnableCustomFilters, selectQueryEngine, selectSettings } from '../../redux/selectors';
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 { dataId, queryEngine, enableCustomFilters, settings } = useSelector(selectResult);
                  value={enableCustomFilters ? query : DISABLED_CUSTOM_FILTERS_MSG}
                  disabled={!enableCustomFilters}
        {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>
          </>
        )}
  UPDATE_ENABLE_CUSTOM_FILTERS = 'update-enable-custom-filters',
/** Action fired when updating the enable_custom_filters flag */
export interface UpdateEnableCustomFilters extends Action<typeof ActionType.UPDATE_ENABLE_CUSTOM_FILTERS> {
  value: boolean;
}

  | UpdateHideColumnMenus
  | UpdateEnableCustomFilters;
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 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 selectBaseEnableCustomFilters = (state: AppState): boolean => state.enableCustomFilters;
const selectSettingsEnableCustomFilters = createSelector(
  [selectSettings],
  (settings) => settings?.enable_custom_filters,
);
export const selectEnableCustomFilters = createSelector(
  [selectSettingsEnableCustomFilters, selectBaseEnableCustomFilters],
  (settingsEnableCustomFilters, enableCustomFilters) => settingsEnableCustomFilters ?? enableCustomFilters,
);
  enable_custom_filters: boolean;
  enable_custom_filters: false,
  enableCustomFilters: boolean;
enable_custom_filters = False
enable_custom_filters = False
        "enable_custom_filters": True,
        assert settings["enable_custom_filters"]
        assert not settings["enable_custom_filters"]
        "enable_custom_filters": True,
        assert settings["enable_custom_filters"]
        assert not settings["enable_custom_filters"]
def setup_function(function):
    global_state.cleanup()


def teardown_function(function):
    global_state.cleanup()


        enable_custom_filters=True,
            enable_custom_filters=True,
            enable_custom_filters=True,
        global_state.set_app_settings(dict(enable_custom_filters=True))

        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))