From 0e0bb3582f9de9c1a822aed5145fcc3def0895cf Mon Sep 17 00:00:00 2001 From: s-kawamura-upgrade Date: Wed, 8 Apr 2026 17:38:24 +0900 Subject: [PATCH] feat(web): add ALLOW_INLINE_STYLES env var to opt-in inline CSS in Markdown rendering (#34719) Co-authored-by: Claude Opus 4.6 (1M context) --- docker/.env.example | 5 +++++ docker/docker-compose-template.yaml | 1 + docker/docker-compose.yaml | 2 ++ web/.env.example | 3 +++ web/app/components/base/markdown/streamdown-wrapper.tsx | 7 ++++++- web/config/index.ts | 1 + web/docker/entrypoint.sh | 1 + web/env.ts | 6 ++++++ 8 files changed, 25 insertions(+), 1 deletion(-) diff --git a/docker/.env.example b/docker/.env.example index e32c9108e8..c046f6d378 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -1176,6 +1176,11 @@ TEXT_GENERATION_TIMEOUT_MS=60000 # Enable the experimental vinext runtime shipped in the image. EXPERIMENTAL_ENABLE_VINEXT=false +# Allow inline style attributes in Markdown rendering. +# Enable this if your workflows use Jinja2 templates with styled HTML. +# Only recommended for self-hosted deployments with trusted content. +ALLOW_INLINE_STYLES=false + # Allow rendering unsafe URLs which have "data:" scheme. ALLOW_UNSAFE_DATA_SCHEME=false diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 0e766da878..4f4b3851f6 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -165,6 +165,7 @@ services: TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000} CSP_WHITELIST: ${CSP_WHITELIST:-} ALLOW_EMBED: ${ALLOW_EMBED:-false} + ALLOW_INLINE_STYLES: ${ALLOW_INLINE_STYLES:-false} ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false} MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai} MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai} diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 1a61573446..3f6a13e78e 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -510,6 +510,7 @@ x-shared-env: &shared-api-worker-env MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-99} TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000} EXPERIMENTAL_ENABLE_VINEXT: ${EXPERIMENTAL_ENABLE_VINEXT:-false} + ALLOW_INLINE_STYLES: ${ALLOW_INLINE_STYLES:-false} ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false} MAX_TREE_DEPTH: ${MAX_TREE_DEPTH:-50} PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata} @@ -875,6 +876,7 @@ services: TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000} CSP_WHITELIST: ${CSP_WHITELIST:-} ALLOW_EMBED: ${ALLOW_EMBED:-false} + ALLOW_INLINE_STYLES: ${ALLOW_INLINE_STYLES:-false} ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false} MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai} MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai} diff --git a/web/.env.example b/web/.env.example index 62d4fa6c56..93cbc22fc8 100644 --- a/web/.env.example +++ b/web/.env.example @@ -48,6 +48,9 @@ NEXT_PUBLIC_CSP_WHITELIST= # Default is not allow to embed into iframe to prevent Clickjacking: https://owasp.org/www-community/attacks/Clickjacking NEXT_PUBLIC_ALLOW_EMBED= +# Allow inline style attributes in Markdown rendering (self-hosted opt-in). +NEXT_PUBLIC_ALLOW_INLINE_STYLES=false + # Allow rendering unsafe URLs which have "data:" scheme. NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME=false diff --git a/web/app/components/base/markdown/streamdown-wrapper.tsx b/web/app/components/base/markdown/streamdown-wrapper.tsx index 46db301adb..e20898135b 100644 --- a/web/app/components/base/markdown/streamdown-wrapper.tsx +++ b/web/app/components/base/markdown/streamdown-wrapper.tsx @@ -16,7 +16,7 @@ import { ThinkBlock, VideoBlock, } from '@/app/components/base/markdown-blocks' -import { ENABLE_SINGLE_DOLLAR_LATEX } from '@/config' +import { ALLOW_INLINE_STYLES, ENABLE_SINGLE_DOLLAR_LATEX } from '@/config' import dynamic from '@/next/dynamic' import { customUrlTransform } from './markdown-utils' import 'katex/dist/katex.min.css' @@ -118,6 +118,11 @@ function buildRehypePlugins(extraPlugins?: PluggableList): PluggableList { // component validates names with `isSafeName()`, so remove it. const clobber = (defaultSanitizeSchema.clobber ?? []).filter(k => k !== 'name') + if (ALLOW_INLINE_STYLES) { + const globalAttrs = mergedAttributes['*'] ?? [] + mergedAttributes['*'] = [...globalAttrs, 'style'] + } + const customSchema: SanitizeSchema = { ...defaultSanitizeSchema, tagNames: [...tagNamesSet], diff --git a/web/config/index.ts b/web/config/index.ts index 999aa754e6..63967763f6 100644 --- a/web/config/index.ts +++ b/web/config/index.ts @@ -304,6 +304,7 @@ export const LOOP_NODE_MAX_COUNT = env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT export const MAX_ITERATIONS_NUM = env.NEXT_PUBLIC_MAX_ITERATIONS_NUM export const MAX_TREE_DEPTH = env.NEXT_PUBLIC_MAX_TREE_DEPTH +export const ALLOW_INLINE_STYLES = env.NEXT_PUBLIC_ALLOW_INLINE_STYLES export const ALLOW_UNSAFE_DATA_SCHEME = env.NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME export const ENABLE_WEBSITE_JINAREADER = env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER export const ENABLE_WEBSITE_FIRECRAWL = env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL diff --git a/web/docker/entrypoint.sh b/web/docker/entrypoint.sh index 50681910d2..3e42d48d0c 100755 --- a/web/docker/entrypoint.sh +++ b/web/docker/entrypoint.sh @@ -30,6 +30,7 @@ export NEXT_PUBLIC_AMPLITUDE_API_KEY=${AMPLITUDE_API_KEY} export NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=${TEXT_GENERATION_TIMEOUT_MS} export NEXT_PUBLIC_CSP_WHITELIST=${CSP_WHITELIST} export NEXT_PUBLIC_ALLOW_EMBED=${ALLOW_EMBED} +export NEXT_PUBLIC_ALLOW_INLINE_STYLES=${ALLOW_INLINE_STYLES:-false} export NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME=${ALLOW_UNSAFE_DATA_SCHEME:-false} export NEXT_PUBLIC_TOP_K_MAX_VALUE=${TOP_K_MAX_VALUE} export NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH} diff --git a/web/env.ts b/web/env.ts index 55709918a8..3623b68462 100644 --- a/web/env.ts +++ b/web/env.ts @@ -19,6 +19,11 @@ const clientSchema = { * Default is not allow to embed into iframe to prevent Clickjacking: https://owasp.org/www-community/attacks/Clickjacking */ NEXT_PUBLIC_ALLOW_EMBED: coercedBoolean.default(false), + /** + * Allow inline style attributes in Markdown rendering. + * Self-hosted opt-in for workflows using styled Jinja2 templates. + */ + NEXT_PUBLIC_ALLOW_INLINE_STYLES: coercedBoolean.default(false), /** * Allow rendering unsafe URLs which have "data:" scheme. */ @@ -153,6 +158,7 @@ export const env = createEnv({ client: clientSchema, experimental__runtimeEnv: { NEXT_PUBLIC_ALLOW_EMBED: isServer ? process.env.NEXT_PUBLIC_ALLOW_EMBED : getRuntimeEnvFromBody('allowEmbed'), + NEXT_PUBLIC_ALLOW_INLINE_STYLES: isServer ? process.env.NEXT_PUBLIC_ALLOW_INLINE_STYLES : getRuntimeEnvFromBody('allowInlineStyles'), NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME: isServer ? process.env.NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME : getRuntimeEnvFromBody('allowUnsafeDataScheme'), NEXT_PUBLIC_AMPLITUDE_API_KEY: isServer ? process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY : getRuntimeEnvFromBody('amplitudeApiKey'), NEXT_PUBLIC_API_PREFIX: isServer ? process.env.NEXT_PUBLIC_API_PREFIX : getRuntimeEnvFromBody('apiPrefix'),