diff --git a/spec/helpers/url_helpers_spec.rb b/spec/helpers/url_helpers_spec.rb
index 1ba7126d0..944a09211 100644
--- a/spec/helpers/url_helpers_spec.rb
+++ b/spec/helpers/url_helpers_spec.rb
@@ -106,5 +106,35 @@ def helper
)
expect(url).to eq("https://github.com/progrium/dokku/releases/tag/v0.4.4")
end
+
+ it "returns nil for javascript: scheme" do
+ url = helper.normalize_url(
+ "javascript:alert(1)", "https://example.com/feed.xml"
+ )
+ expect(url).to be_nil
+ end
+
+ it "returns nil for data: scheme" do
+ url = helper.normalize_url(
+ "data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==",
+ "https://example.com/feed.xml"
+ )
+ expect(url).to be_nil
+ end
+
+ it "returns nil for other non-http schemes" do
+ ["vbscript:msgbox(1)", "file:///etc/passwd"].each do |bad|
+ expect(
+ helper.normalize_url(bad, "https://example.com/feed.xml")
+ ).to be_nil
+ end
+ end
+
+ it "rejects case-mangled javascript: scheme" do
+ url = helper.normalize_url(
+ "JaVaScRiPt:alert(1)", "https://example.com/feed.xml"
+ )
+ expect(url).to be_nil
+ end
end
end
diff --git a/spec/javascript/setup.ts b/spec/javascript/setup.ts
index 52a3e1cbb..58ed016d7 100644
--- a/spec/javascript/setup.ts
+++ b/spec/javascript/setup.ts
@@ -12,6 +12,7 @@ globalThis._ = underscore;
globalThis.Backbone = Backbone;
_.templateSettings = {
+ escape: /\{\{-(.+?)\}\}/g,
evaluate: /\{\{(.+?)\}\}/g,
interpolate: /\{\{=(.+?)\}\}/g,
};
@@ -53,9 +54,9 @@ const templateHTML = [
'
',
'
',
"
",
- ' {{= title }}',
+ ' {{= title }}',
" {{ if (enclosure_url) { }}",
- ' ',
+ ' ',
' ',
" ",
" {{ } }}",
@@ -75,7 +76,7 @@ const templateHTML = [
'
',
' ',
"
",
- ' ',
+ ' ',
' ',
" ",
" ",
diff --git a/spec/javascript/spec/views/story_view_spec.ts b/spec/javascript/spec/views/story_view_spec.ts
index fed627d4e..0d5c4ec9e 100644
--- a/spec/javascript/spec/views/story_view_spec.ts
+++ b/spec/javascript/spec/views/story_view_spec.ts
@@ -144,6 +144,30 @@ describe("StoryView", function () {
assertPropertyRendered(view.el, story, "enclosure_url");
});
+ it("should escape permalink to prevent attribute injection", function () {
+ var injected = new Story({
+ body: "",
+ enclosure_url: null,
+ headline: "x",
+ is_read: false,
+ is_starred: false,
+ keep_unread: false,
+ lead: "",
+ permalink: "\" onclick=\"alert(1)",
+ pretty_date: "",
+ source: "",
+ title: "x",
+ });
+ var injectedView = new StoryView({ model: injected });
+ injectedView.render();
+
+ var anchors = injectedView.el.querySelectorAll("a[href]");
+ anchors.forEach(function (a) {
+ expect(a.getAttribute("onclick")).toBeNull();
+ expect(a.getAttribute("href")).toBe("\" onclick=\"alert(1)");
+ });
+ });
+
describe("Handling click on story", function () {
let toggleStub;
diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb
index c444da0af..2d47c2bc4 100644
--- a/spec/repositories/story_repository_spec.rb
+++ b/spec/repositories/story_repository_spec.rb
@@ -49,13 +49,29 @@ def create_feed(url: "http://blog.golang.org/feed.atom")
summary: "",
content: ""
).as_null_object
- allow(described_class).to receive(:normalize_url)
+ allow(described_class).to receive(:normalize_url) { |url, _base| url }
expect(Story).to receive(:create).with(hash_including(enclosure_url: "http://example.com/audio.mp3"))
described_class.add(entry, feed)
end
+ it "drops a javascript: enclosure_url" do
+ feed = create_feed
+ entry = instance_double(
+ Feedjira::Parser::ITunesRSSItem,
+ url: "http://example.com/post",
+ enclosure_url: "javascript:alert(1)",
+ title: "",
+ summary: "",
+ content: ""
+ ).as_null_object
+
+ expect(Story).to receive(:create).with(hash_including(enclosure_url: nil))
+
+ described_class.add(entry, feed)
+ end
+
it "does not set the enclosure url when not present" do
feed = create_feed
entry = instance_double(
@@ -429,6 +445,38 @@ def create_feed(url: "http://blog.golang.org/feed.atom")
expect(described_class.extract_url(entry, feed)).to be_nil
end
+
+ it "drops a javascript: entry url" do
+ feed = double(url: "http://github.com")
+ entry = double(url: "javascript:alert(1)")
+
+ expect(described_class.extract_url(entry, feed)).to be_nil
+ end
+
+ it "falls through to enclosure_url when entry url is javascript:" do
+ feed = double(url: "http://github.com")
+ entry = double(
+ url: "javascript:alert(1)",
+ enclosure_url: "https://example.com/audio.mp3"
+ )
+
+ expect(described_class.extract_url(entry, feed))
+ .to eq("https://example.com/audio.mp3")
+ end
+
+ it "drops a javascript: enclosure_url fallback" do
+ feed = double(url: "http://github.com")
+ entry = double(url: nil, enclosure_url: "javascript:alert(1)")
+
+ expect(described_class.extract_url(entry, feed)).to be_nil
+ end
+
+ it "returns nil for an unparseable enclosure_url fallback" do
+ feed = double(url: "http://github.com")
+ entry = double(url: nil, enclosure_url: "http://[invalid")
+
+ expect(described_class.extract_url(entry, feed)).to be_nil
+ end
end
describe ".extract_title" do