Support allowlist for hyperlink schemes in Markdown content (#1702)

This commit is contained in:
GitStart 2023-02-16 00:16:38 +03:00 committed by GitHub
parent 9818e0e669
commit 0990ab4cb2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 50 additions and 6 deletions

View file

@ -229,6 +229,11 @@ The following environment variables configure Livebook:
iframe. Set it to "true" to enable it. If you do enable it, then the application iframe. Set it to "true" to enable it. If you do enable it, then the application
must run with HTTPS. must run with HTTPS.
* LIVEBOOK_ALLOW_URI_SCHEMES - sets addtional extra hyperlink protocols to
the Markdown content. Livebook sanitizes links in Markdown, allowing only a few
standard protocols by default (such as http and https). Set a comma-separated list of
protocols to configure additional protocols.
<!-- Environment variables --> <!-- Environment variables -->
When running Livebook Desktop, Livebook will invoke on boot a file named When running Livebook Desktop, Livebook will invoke on boot a file named

View file

@ -125,6 +125,7 @@ const Cell = {
"data-smart-cell-js-view-ref", "data-smart-cell-js-view-ref",
null null
), ),
protocols: getAttributeOrThrow(this.el, "data-protocols"),
}; };
}, },
@ -216,6 +217,7 @@ const Cell = {
const markdown = new Markdown(markdownContainer, source, { const markdown = new Markdown(markdownContainer, source, {
baseUrl: this.props.sessionPath, baseUrl: this.props.sessionPath,
emptyText: "Empty markdown cell", emptyText: "Empty markdown cell",
extraProtocol: this.props.protocols.replace(/\s+/g, "").split(","),
}); });
liveEditor.onChange((newSource) => { liveEditor.onChange((newSource) => {

View file

@ -25,11 +25,16 @@ import { escapeHtml } from "../lib/utils";
* Renders markdown content in the given container. * Renders markdown content in the given container.
*/ */
class Markdown { class Markdown {
constructor(container, content, { baseUrl = null, emptyText = "" } = {}) { constructor(
container,
content,
{ baseUrl = null, emptyText = "", extraProtocol = [] } = {}
) {
this.container = container; this.container = container;
this.content = content; this.content = content;
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.emptyText = emptyText; this.emptyText = emptyText;
this.extraProtocol = extraProtocol;
this._render(); this._render();
} }
@ -62,7 +67,7 @@ class Markdown {
.use(remarkRehype, { allowDangerousHtml: true }) .use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw) .use(rehypeRaw)
.use(rehypeExpandUrls, { baseUrl: this.baseUrl }) .use(rehypeExpandUrls, { baseUrl: this.baseUrl })
.use(rehypeSanitize, sanitizeSchema()) .use(rehypeSanitize, sanitizeSchema(this.extraProtocol))
.use(rehypeKatex) .use(rehypeKatex)
.use(rehypeMermaid) .use(rehypeMermaid)
.use(rehypeExternalLinks, { baseUrl: this.baseUrl }) .use(rehypeExternalLinks, { baseUrl: this.baseUrl })
@ -98,15 +103,20 @@ export default Markdown;
// Plugins // Plugins
function sanitizeSchema() { function sanitizeSchema(extraProtocol) {
// Allow class and style attributes on tags for syntax highlighting, // Allow class and style attributes on tags for syntax highlighting,
// remarkMath tags, or user-written styles // remarkMath tags, or user-written styles
return { return {
...defaultSchema, ...defaultSchema,
attributes: { attributes: {
...defaultSchema.attributes, ...defaultSchema.attributes,
"*": [...(defaultSchema.attributes["*"] || []), "className", "style"], "*": [...(defaultSchema.attributes["*"] || []), "className", "style"],
}, },
protocols: {
...defaultSchema.protocols,
href: [...defaultSchema.protocols.href, ...extraProtocol],
},
}; };
} }

View file

@ -33,7 +33,8 @@ config :livebook,
shutdown_callback: nil, shutdown_callback: nil,
storage: Livebook.Storage.Ets, storage: Livebook.Storage.Ets,
update_instructions_url: nil, update_instructions_url: nil,
within_iframe: false within_iframe: false,
allowed_uri_schemes: []
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.

View file

@ -176,6 +176,10 @@ defmodule Livebook do
Livebook.Config.update_instructions_url!("LIVEBOOK_UPDATE_INSTRUCTIONS_URL") do Livebook.Config.update_instructions_url!("LIVEBOOK_UPDATE_INSTRUCTIONS_URL") do
config :livebook, :update_instructions_url, update_instructions_url config :livebook, :update_instructions_url, update_instructions_url
end end
if allowed_uri_schemes = Livebook.Config.allowed_uri_schemes!("LIVEBOOK_ALLOW_URI_SCHEMES") do
config :livebook, :allowed_uri_schemes, allowed_uri_schemes
end
end end
@doc """ @doc """

View file

@ -194,6 +194,14 @@ defmodule Livebook.Config do
@feature_flags[key] @feature_flags[key]
end end
@doc """
Return list of added uri schemes.
"""
@spec allowed_uri_schemes() :: []
def allowed_uri_schemes() do
Application.fetch_env!(:livebook, :allowed_uri_schemes)
end
## Parsing ## Parsing
@doc """ @doc """
@ -430,4 +438,13 @@ defmodule Livebook.Config do
IO.puts("\nERROR!!! [Livebook] " <> message) IO.puts("\nERROR!!! [Livebook] " <> message)
System.halt(1) System.halt(1)
end end
@doc """
Parses and validates allowed URI schemes from env.
"""
def allowed_uri_schemes!(env) do
if schemes = System.get_env(env) do
String.split(schemes, ",", trim: true)
end
end
end end

View file

@ -5,7 +5,7 @@ defmodule LivebookWeb.SessionLive do
import LivebookWeb.SessionHelpers import LivebookWeb.SessionHelpers
import Livebook.Utils, only: [format_bytes: 1] import Livebook.Utils, only: [format_bytes: 1]
alias Livebook.{Sessions, Session, Delta, Notebook, Runtime, LiveMarkdown, Secrets} alias Livebook.{Config, Sessions, Session, Delta, Notebook, Runtime, LiveMarkdown, Secrets}
alias Livebook.Notebook.{Cell, ContentLoader} alias Livebook.Notebook.{Cell, ContentLoader}
alias Livebook.JSInterop alias Livebook.JSInterop
alias Livebook.Hubs alias Livebook.Hubs
@ -63,7 +63,8 @@ defmodule LivebookWeb.SessionLive do
page_title: get_page_title(data.notebook.name), page_title: get_page_title(data.notebook.name),
saved_secrets: get_saved_secrets(), saved_secrets: get_saved_secrets(),
select_secret_ref: nil, select_secret_ref: nil,
select_secret_options: nil select_secret_options: nil,
protocols: Config.allowed_uri_schemes() |> Enum.join(",")
) )
|> assign_private(data: data) |> assign_private(data: data)
|> prune_outputs() |> prune_outputs()
@ -270,6 +271,7 @@ defmodule LivebookWeb.SessionLive do
session_id={@session.id} session_id={@session.id}
session_pid={@session.pid} session_pid={@session.pid}
client_id={@client_id} client_id={@client_id}
protocols={@protocols}
runtime={@data_view.runtime} runtime={@data_view.runtime}
installing?={@data_view.installing?} installing?={@data_view.installing?}
cell_view={@data_view.setup_cell_view} cell_view={@data_view.setup_cell_view}
@ -289,6 +291,7 @@ defmodule LivebookWeb.SessionLive do
id={section_view.id} id={section_view.id}
index={index} index={index}
session_id={@session.id} session_id={@session.id}
protocols={@protocols}
session_pid={@session.pid} session_pid={@session.pid}
client_id={@client_id} client_id={@client_id}
runtime={@data_view.runtime} runtime={@data_view.runtime}

View file

@ -10,6 +10,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
id={"cell-#{@cell_view.id}"} id={"cell-#{@cell_view.id}"}
phx-hook="Cell" phx-hook="Cell"
data-cell-id={@cell_view.id} data-cell-id={@cell_view.id}
data-protocols={@protocols}
data-focusable-id={@cell_view.id} data-focusable-id={@cell_view.id}
data-type={@cell_view.type} data-type={@cell_view.type}
data-session-path={Routes.session_path(@socket, :page, @session_id)} data-session-path={Routes.session_path(@socket, :page, @session_id)}

View file

@ -142,6 +142,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
session_id={@session_id} session_id={@session_id}
session_pid={@session_pid} session_pid={@session_pid}
client_id={@client_id} client_id={@client_id}
protocols={@protocols}
runtime={@runtime} runtime={@runtime}
installing?={@installing?} installing?={@installing?}
cell_view={cell_view} cell_view={cell_view}