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:
David Taylor 2024-11-19 11:44:17 +00:00 committed by GitHub
parent 6b9c66054c
commit b10be23533
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 34 additions and 7 deletions

View File

@ -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

View File

@ -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