mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-12-10 05:36:15 +08:00
Support allowlist for hyperlink schemes in Markdown content (#1702)
This commit is contained in:
parent
9818e0e669
commit
0990ab4cb2
9 changed files with 50 additions and 6 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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 """
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue