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

View file

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

View file

@ -33,7 +33,8 @@ config :livebook,
shutdown_callback: nil,
storage: Livebook.Storage.Ets,
update_instructions_url: nil,
within_iframe: false
within_iframe: false,
allowed_uri_schemes: []
# Import environment specific config. This must remain at the bottom
# 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
config :livebook, :update_instructions_url, update_instructions_url
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
@doc """

View file

@ -194,6 +194,14 @@ defmodule Livebook.Config do
@feature_flags[key]
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
@doc """
@ -430,4 +438,13 @@ defmodule Livebook.Config do
IO.puts("\nERROR!!! [Livebook] " <> message)
System.halt(1)
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

View file

@ -5,7 +5,7 @@ defmodule LivebookWeb.SessionLive do
import LivebookWeb.SessionHelpers
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.JSInterop
alias Livebook.Hubs
@ -63,7 +63,8 @@ defmodule LivebookWeb.SessionLive do
page_title: get_page_title(data.notebook.name),
saved_secrets: get_saved_secrets(),
select_secret_ref: nil,
select_secret_options: nil
select_secret_options: nil,
protocols: Config.allowed_uri_schemes() |> Enum.join(",")
)
|> assign_private(data: data)
|> prune_outputs()
@ -270,6 +271,7 @@ defmodule LivebookWeb.SessionLive do
session_id={@session.id}
session_pid={@session.pid}
client_id={@client_id}
protocols={@protocols}
runtime={@data_view.runtime}
installing?={@data_view.installing?}
cell_view={@data_view.setup_cell_view}
@ -289,6 +291,7 @@ defmodule LivebookWeb.SessionLive do
id={section_view.id}
index={index}
session_id={@session.id}
protocols={@protocols}
session_pid={@session.pid}
client_id={@client_id}
runtime={@data_view.runtime}

View file

@ -10,6 +10,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
id={"cell-#{@cell_view.id}"}
phx-hook="Cell"
data-cell-id={@cell_view.id}
data-protocols={@protocols}
data-focusable-id={@cell_view.id}
data-type={@cell_view.type}
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_pid={@session_pid}
client_id={@client_id}
protocols={@protocols}
runtime={@runtime}
installing?={@installing?}
cell_view={cell_view}