CVE-2023-46134 (Other, published 2023-10-25T21:15Z)
Description: 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.

Severity: nan (CVSS v3 base score: 9.8)
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/man-group/dtale
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 message: Updated 'Custom Filter' feature to be disabled by default


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