apache/superset · error · ValidationError

CSS contains a disallowed construct ({label}).

Error message

CSS contains a disallowed construct ({label}).

What it means

Custom CSS validator on dashboards: stored CSS is re-served into the dashboard page, so constructs that can execute script or load remote resources are rejected at validation time. Matching patterns include script schemes (javascript:, vbscript:, livescript:, mocha:), legacy IE expression(), and remote url() imports. The error names the offending construct label.

Source

Thrown at superset/dashboards/schemas.py:173

    Lightweight input hardening for the user-supplied ``css`` field, which is
    persisted and re-served into the dashboard page. Blocks ``expression(``,
    script-scheme URIs (e.g. ``javascript:``), ``@import``, and ``url(...)``
    referencing a script scheme, while leaving ordinary styling intact.

    CSS escape sequences (e.g. ``\\6a avascript:``) are not expanded before
    matching, so this validator is a first-line filter and not a complete XSS
    sanitiser; it should not be treated as a substitute for other defences.
    """
    if not value:
        return
    if isinstance(value, (bytes, bytearray)):
        text = value.decode("utf-8", errors="ignore")
    else:
        text = value
    for label, pattern in _DANGEROUS_CSS_PATTERNS:
        if pattern.search(text):
            raise ValidationError(f"CSS contains a disallowed construct ({label}).")


class SharedLabelsColorsField(fields.Field):
    """
    A custom field that accepts either a list of strings or a dictionary.
    """

    def _deserialize(
        self,
        value: Union[list[str], dict[str, str]],
        attr: Union[str, None],
        data: Union[Mapping[str, Any], None],
        **kwargs: dict[str, Any],
    ) -> list[str]:
        if isinstance(value, list):
            if all(isinstance(item, str) for item in value):
                return value
        elif isinstance(value, dict):

View on GitHub (pinned to f4587218dd)

Solutions

  1. Remove the construct named in the error message label (e.g. drop the expression() rule or the javascript: URL).
  2. Host assets locally or use data: URIs / relative paths for images, which the validator explicitly allows.
  3. If a false positive (the pattern appears in an innocent comment/string), reword the comment or remove the literal token.
  4. Note the documented limitation: CSS escapes are not expanded, so treat this as a first-line filter and keep CSP as a defence in depth.

Example fix

/* before */
.chart { width: expression(document.body.clientWidth); background: url(javascript:alert(1)); }

/* after */
.chart { width: 95%; background: url('/static/img/bg.png'); }
Defensive patterns

Strategy: validation

Validate before calling

import re
_BAD = [re.compile(r"expression\s*\(", re.I), re.compile(r"(?:javascript|vbscript|livescript|mocha)\s*:")]
def css_looks_safe(css: str) -> bool:
    return not any(p.search(css) for p in _BAD)

Try / catch

from marshmallow import ValidationError
try:
    validate_css(css)
except ValidationError as ex:
    label = str(ex.messages)  # names the disallowed construct
    raise ValueError(f"remove forbidden CSS construct: {label}")

Prevention

When it happens

Trigger: PUT/POST a dashboard with css containing 'expression(' anywhere, a javascript:/vbscript: scheme token, or a url() pointing at a remote stylesheet (e.g. @import url(https://evil.com/x.css)).

Common situations: Pasting CSS from old IE-era snippets; attempting to load web fonts or external stylesheets via url(); obfuscated CSS where a scheme string appears inside a comment or string value (the check is textual, so false positives are possible).

Related errors


AI-assisted analysis of apache/superset@f4587218dd (2026-08-14). Data as JSON: /api/errors/e4cd786ec9f9716a. Report an issue: GitHub.