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)
end
# Prepare the HTML document
html = <<~HTML
# Prepare the inner (untrusted) HTML document
untrusted_html = <<~HTML
<!DOCTYPE html>
<html>
<head>
@ -39,11 +39,33 @@ module DiscourseAi
</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["Content-Security-Policy"] = "script-src 'unsafe-inline';"
# 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
private

View File

@ -18,6 +18,10 @@ RSpec.describe DiscourseAi::AiBot::ArtifactsController do
)
end
def parse_srcdoc(html)
Nokogiri.HTML5(html).at_css("iframe")["srcdoc"]
end
before do
SiteSetting.discourse_ai_enabled = true
SiteSetting.ai_artifact_security = "strict"
@ -46,9 +50,10 @@ RSpec.describe DiscourseAi::AiBot::ArtifactsController do
sign_in(user)
get "/discourse-ai/ai-bot/artifacts/#{artifact.id}"
expect(response.status).to eq(200)
expect(response.body).to include(artifact.html)
expect(response.body).to include(artifact.css)
expect(response.body).to include(artifact.js)
untrusted_html = parse_srcdoc(response.body)
expect(untrusted_html).to include(artifact.html)
expect(untrusted_html).to include(artifact.css)
expect(untrusted_html).to include(artifact.js)
end
end
@ -58,7 +63,7 @@ RSpec.describe DiscourseAi::AiBot::ArtifactsController do
it "shows artifact without authentication" do
get "/discourse-ai/ai-bot/artifacts/#{artifact.id}"
expect(response.status).to eq(200)
expect(response.body).to include(artifact.html)
expect(parse_srcdoc(response.body)).to include(artifact.html)
end
end