Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit b10be23

Browse files
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.
1 parent 6b9c660 commit b10be23

File tree

2 files changed

+34
-7
lines changed

2 files changed

+34
-7
lines changed

app/controllers/discourse_ai/ai_bot/artifacts_controller.rb

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ def show
1919
raise Discourse::NotFound if !guardian.can_see?(post)
2020
end
2121

22-
# Prepare the HTML document
23-
html = <<~HTML
22+
# Prepare the inner (untrusted) HTML document
23+
untrusted_html = <<~HTML
2424
<!DOCTYPE html>
2525
<html>
2626
<head>
@@ -39,11 +39,33 @@ def show
3939
</html>
4040
HTML
4141

42+
# Prepare the outer (trusted) HTML document
43+
trusted_html = <<~HTML
44+
<!DOCTYPE html>
45+
<html>
46+
<head>
47+
<meta charset="UTF-8">
48+
<title>#{ERB::Util.html_escape(artifact.name)}</title>
49+
<style>
50+
html, body, iframe {
51+
margin: 0;
52+
padding: 0;
53+
width: 100%;
54+
height: 100%;
55+
}
56+
</style>
57+
</head>
58+
<body>
59+
<iframe sandbox="allow-scripts allow-forms" height="100%" width="100%" srcdoc="#{ERB::Util.html_escape(untrusted_html)}" frameborder="0"></iframe>
60+
</body>
61+
</html>
62+
HTML
63+
4264
response.headers.delete("X-Frame-Options")
4365
response.headers["Content-Security-Policy"] = "script-src 'unsafe-inline';"
4466

4567
# Render the content
46-
render html: html.html_safe, layout: false, content_type: "text/html"
68+
render html: trusted_html.html_safe, layout: false, content_type: "text/html"
4769
end
4870

4971
private

spec/requests/ai_bot/artifacts_controller_spec.rb

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
)
1919
end
2020

21+
def parse_srcdoc(html)
22+
Nokogiri.HTML5(html).at_css("iframe")["srcdoc"]
23+
end
24+
2125
before do
2226
SiteSetting.discourse_ai_enabled = true
2327
SiteSetting.ai_artifact_security = "strict"
@@ -46,9 +50,10 @@
4650
sign_in(user)
4751
get "/discourse-ai/ai-bot/artifacts/#{artifact.id}"
4852
expect(response.status).to eq(200)
49-
expect(response.body).to include(artifact.html)
50-
expect(response.body).to include(artifact.css)
51-
expect(response.body).to include(artifact.js)
53+
untrusted_html = parse_srcdoc(response.body)
54+
expect(untrusted_html).to include(artifact.html)
55+
expect(untrusted_html).to include(artifact.css)
56+
expect(untrusted_html).to include(artifact.js)
5257
end
5358
end
5459

@@ -58,7 +63,7 @@
5863
it "shows artifact without authentication" do
5964
get "/discourse-ai/ai-bot/artifacts/#{artifact.id}"
6065
expect(response.status).to eq(200)
61-
expect(response.body).to include(artifact.html)
66+
expect(parse_srcdoc(response.body)).to include(artifact.html)
6267
end
6368
end
6469

0 commit comments

Comments
 (0)