FIX: Ensure artifacts are sandboxed, even when visited directly (#921)
It's important that artifacts are never given 'same origin' access to the forum domain, so that they cannot access cookies, or make authenticated HTTP requests. So even when visiting the URL directly, we need to wrap them in a sandboxed iframe.
This commit is contained in:
parent
6b9c66054c
commit
b10be23533
|
@ -19,8 +19,8 @@ module DiscourseAi
|
||||||
raise Discourse::NotFound if !guardian.can_see?(post)
|
raise Discourse::NotFound if !guardian.can_see?(post)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Prepare the HTML document
|
# Prepare the inner (untrusted) HTML document
|
||||||
html = <<~HTML
|
untrusted_html = <<~HTML
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
@ -39,11 +39,33 @@ module DiscourseAi
|
||||||
</html>
|
</html>
|
||||||
HTML
|
HTML
|
||||||
|
|
||||||
|
# Prepare the outer (trusted) HTML document
|
||||||
|
trusted_html = <<~HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>#{ERB::Util.html_escape(artifact.name)}</title>
|
||||||
|
<style>
|
||||||
|
html, body, iframe {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<iframe sandbox="allow-scripts allow-forms" height="100%" width="100%" srcdoc="#{ERB::Util.html_escape(untrusted_html)}" frameborder="0"></iframe>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML
|
||||||
|
|
||||||
response.headers.delete("X-Frame-Options")
|
response.headers.delete("X-Frame-Options")
|
||||||
response.headers["Content-Security-Policy"] = "script-src 'unsafe-inline';"
|
response.headers["Content-Security-Policy"] = "script-src 'unsafe-inline';"
|
||||||
|
|
||||||
# Render the content
|
# Render the content
|
||||||
render html: html.html_safe, layout: false, content_type: "text/html"
|
render html: trusted_html.html_safe, layout: false, content_type: "text/html"
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -18,6 +18,10 @@ RSpec.describe DiscourseAi::AiBot::ArtifactsController do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def parse_srcdoc(html)
|
||||||
|
Nokogiri.HTML5(html).at_css("iframe")["srcdoc"]
|
||||||
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
SiteSetting.discourse_ai_enabled = true
|
SiteSetting.discourse_ai_enabled = true
|
||||||
SiteSetting.ai_artifact_security = "strict"
|
SiteSetting.ai_artifact_security = "strict"
|
||||||
|
@ -46,9 +50,10 @@ RSpec.describe DiscourseAi::AiBot::ArtifactsController do
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
get "/discourse-ai/ai-bot/artifacts/#{artifact.id}"
|
get "/discourse-ai/ai-bot/artifacts/#{artifact.id}"
|
||||||
expect(response.status).to eq(200)
|
expect(response.status).to eq(200)
|
||||||
expect(response.body).to include(artifact.html)
|
untrusted_html = parse_srcdoc(response.body)
|
||||||
expect(response.body).to include(artifact.css)
|
expect(untrusted_html).to include(artifact.html)
|
||||||
expect(response.body).to include(artifact.js)
|
expect(untrusted_html).to include(artifact.css)
|
||||||
|
expect(untrusted_html).to include(artifact.js)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -58,7 +63,7 @@ RSpec.describe DiscourseAi::AiBot::ArtifactsController do
|
||||||
it "shows artifact without authentication" do
|
it "shows artifact without authentication" do
|
||||||
get "/discourse-ai/ai-bot/artifacts/#{artifact.id}"
|
get "/discourse-ai/ai-bot/artifacts/#{artifact.id}"
|
||||||
expect(response.status).to eq(200)
|
expect(response.status).to eq(200)
|
||||||
expect(response.body).to include(artifact.html)
|
expect(parse_srcdoc(response.body)).to include(artifact.html)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue