Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Original document security settings apply to speedreader document #21958

Merged
merged 8 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion browser/speedreader/speedreader_browsertest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@ const char kTestPageSimple[] = "/simple.html";
const char kTestPageReadable[] = "/speedreader/article/guardian.html";
const char kTestEsPageReadable[] = "/speedreader/article/es.html";
const char kTestPageReadableOnUnreadablePath[] =
"/speedreader/rewriter/pages/news_pages/abcnews.com/distilled.html";
"/speedreader/rewriter/pages/news_pages/abcnews.com/original.html";
const char kTestPageRedirect[] = "/articles/redirect_me.html";
const char kTestXml[] = "/speedreader/article/rss.xml";
const char kTestTtsSimple[] = "/speedreader/article/simple.html";
const char kTestTtsTags[] = "/speedreader/article/tags.html";
const char kTestTtsStructure[] = "/speedreader/article/structure.html";
const char kTestErrorPage[] = "/speedreader/article/page_not_reachable.html";
const char kTestCSPHtmlPage[] = "/speedreader/article/csp_html.html";
const char kTestCSPHttpPage[] = "/speedreader/article/csp_http.html";

class SpeedReaderBrowserTest : public InProcessBrowserTest {
public:
Expand Down Expand Up @@ -878,6 +880,21 @@ IN_PROC_BROWSER_TEST_F(SpeedReaderBrowserTest, ErrorPage) {
tab_helper()->PageDistillState()));
}

IN_PROC_BROWSER_TEST_F(SpeedReaderBrowserTest, Csp) {
ToggleSpeedreader();

for (const auto* page : {kTestCSPHtmlPage, kTestCSPHttpPage}) {
content::WebContentsConsoleObserver console_observer(ActiveWebContents());
console_observer.SetPattern(
"Refused to load the image 'https://a.test/should_fail.png' because it "
"violates the following Content Security Policy directive: \"img-src "
"'none'\".*");
NavigateToPageSynchronously(page, WindowOpenDisposition::CURRENT_TAB);

EXPECT_TRUE(console_observer.Wait());
}
}

class SpeedReaderWithDistillationServiceBrowserTest
: public SpeedReaderBrowserTest {
public:
Expand Down
5 changes: 1 addition & 4 deletions components/speedreader/renderer/speedreader_js_handler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,11 @@ SpeedreaderJSHandler::~SpeedreaderJSHandler() = default;
// static
void SpeedreaderJSHandler::Install(
base::WeakPtr<SpeedreaderRenderFrameObserver> owner,
int32_t isolated_world_id) {
v8::Local<v8::Context> context) {
DCHECK(owner);
v8::Isolate* isolate = blink::MainThreadIsolate();
v8::HandleScope handle_scope(isolate);

v8::Local<v8::Context> context =
owner->render_frame()->GetWebFrame()->GetScriptContextFromWorldId(
isolate, isolated_world_id);
if (context.IsEmpty()) {
return;
}
Expand Down
2 changes: 1 addition & 1 deletion components/speedreader/renderer/speedreader_js_handler.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class SpeedreaderJSHandler final : public gin::Wrappable<SpeedreaderJSHandler> {
SpeedreaderJSHandler& operator=(const SpeedreaderJSHandler&) = delete;

static void Install(base::WeakPtr<SpeedreaderRenderFrameObserver> owner,
int32_t isolated_world_id);
v8::Local<v8::Context> context);

private:
explicit SpeedreaderJSHandler(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ SpeedreaderRenderFrameObserver::SpeedreaderRenderFrameObserver(

SpeedreaderRenderFrameObserver::~SpeedreaderRenderFrameObserver() = default;

void SpeedreaderRenderFrameObserver::DidClearWindowObject() {
if (!render_frame()->IsMainFrame()) {
void SpeedreaderRenderFrameObserver::DidCreateScriptContext(
v8::Local<v8::Context> context,
int32_t world_id) {
if (!render_frame() || !render_frame()->IsMainFrame() ||
isolated_world_id_ != world_id) {
return;
}
SpeedreaderJSHandler::Install(weak_ptr_factory_.GetWeakPtr(),
isolated_world_id_);

SpeedreaderJSHandler::Install(weak_ptr_factory_.GetWeakPtr(), context);
}

void SpeedreaderRenderFrameObserver::OnDestruct() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class SpeedreaderRenderFrameObserver : public content::RenderFrameObserver {
~SpeedreaderRenderFrameObserver() override;

// RenderFrameObserver implementation.
void DidClearWindowObject() override;
void DidCreateScriptContext(v8::Local<v8::Context> context,
int32_t world_id) override;

private:
// RenderFrameObserver implementation.
Expand Down
30 changes: 30 additions & 0 deletions components/speedreader/resources/speedreader-desktop.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ class speedreaderUtils {
return document.getElementById(id)
}

static adoptStyles = () => {
for (const style of document.styleSheets) {
style.disabled = true
}

const braveStyles =
document.querySelectorAll('script[type="brave-style-data"]')
for (const styleData of braveStyles) {
const style = new CSSStyleSheet()
style.replaceSync(styleData.innerText)
document.adoptedStyleSheets.push(style)
}
document.body.hidden = false
}

static initShowOriginalLink = () => {
const link = this.$(this.showOriginalLinkId)
if (!link)
Expand Down Expand Up @@ -67,6 +82,20 @@ class speedreaderUtils {
return
}

{
const spans =
this.$(this.contentDivId)?.querySelectorAll('p > span')

for (const span of spans) {
const p = span.parentNode

while (span.childNodes.length > 0) {
p.insertBefore(span.firstChild, span)
}
p.removeChild(span)
}
}

let textToSpeak = 0

const makeParagraph = (elem) => {
Expand Down Expand Up @@ -267,6 +296,7 @@ class speedreaderUtils {
speedreaderUtils.defaultSpeedreaderData,
window.speedreaderData)

speedreaderUtils.adoptStyles()
speedreaderUtils.initShowOriginalLink()
speedreaderUtils.calculateReadtime()
speedreaderUtils.initTextToSpeak()
Expand Down
41 changes: 28 additions & 13 deletions components/speedreader/rust/lib/src/readability/src/extractor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ pub struct Meta {
pub description: Option<String>,
pub charset: Option<String>,
pub last_modified: Option<OffsetDateTime>,
pub preserved_meta: Vec<Handle>,
}

impl Meta {
Expand All @@ -102,6 +103,7 @@ impl Meta {
};
self.charset = self.charset.or(other.charset);
self.last_modified = self.last_modified.or(other.last_modified);
self.preserved_meta.extend(other.preserved_meta);
self
}
}
Expand Down Expand Up @@ -188,14 +190,14 @@ pub fn extract_metadata(dom: &Sink) -> Meta {
}
} else if let Some(charset) = attribute.get(local_name!("charset")) {
meta_tags.charset = Some(charset.to_string());
} else if attribute
.get(local_name!("http-equiv"))
.map(|e| e.to_ascii_lowercase() == "content-type")
.unwrap_or(false)
{
if let Some(content) = attribute.get(local_name!("content")) {
if let Some(charset) = content.split("charset=").nth(1) {
meta_tags.charset = Some(charset.trim().to_string());
} else if let Some(http_equiv) = attribute.get(local_name!("http-equiv")) {
meta_tags.preserved_meta.push(node.clone());

if http_equiv.to_ascii_lowercase() == "content-type" {
if let Some(content) = attribute.get(local_name!("content")) {
if let Some(charset) = content.split("charset=").nth(1) {
meta_tags.charset = Some(charset.trim().to_string());
}
}
}
}
Expand Down Expand Up @@ -285,11 +287,24 @@ pub fn extract_dom(

// Our CSS formats based on id="article".
dom::set_attr("id", "article", body.clone(), true);
dom::set_attr("hidden", "true", body.clone(), true);
body.to_string()
}
_ => top_candidate.to_string(),
};

for node in meta.preserved_meta.iter() {
if let Some(data) = node.as_element() {
let attributes = data.attributes.borrow();

let mut val: String = String::from("<meta ");
for attr in attributes.map.iter() {
val += &format!(" {}=\"{}\" ", attr.0.local, attr.1.value);
}
content = val + ">" + &content;
}
}

if let Some(ref charset) = meta.charset {
// Since we strip out the entire head, we need to include charset if one
// was provided. Otherwise the browser will use the default encoding,
Expand All @@ -305,18 +320,18 @@ pub fn extract_dom(
if theme.is_some() || font_family.is_some() || font_size.is_some() || column_width.is_some() {
let mut header: String = String::from("<html");
if let Some(theme) = theme {
header = [header, format!(" data-theme=\"{}\"", theme)].concat();
header += &format!(" data-theme=\"{}\"", theme);
}
if let Some(font_family) = font_family {
header = [header, format!(" data-font-family=\"{}\"", font_family)].concat();
header += &format!(" data-font-family=\"{}\"", font_family);
}
if let Some(font_size) = font_size {
header = [header, format!(" data-font-size=\"{}\"", font_size)].concat();
header += &format!(" data-font-size=\"{}\"", font_size);
}
if let Some(column_width) = column_width {
header = [header, format!(" data-column-width=\"{}\"", column_width)].concat();
header += &format!(" data-column-width=\"{}\"", column_width);
}
content = [header, ">".to_string(), content, "</html>".to_string()].concat();
content = header + ">" + &content + "</html>";
}

Ok(Product { meta, content })
Expand Down
35 changes: 21 additions & 14 deletions components/speedreader/speedreader_rewriter_service.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include "brave/components/speedreader/speedreader_rewriter_service.h"

#include <string_view>
#include <utility>

#include "base/base64.h"
Expand Down Expand Up @@ -38,25 +39,31 @@ constexpr const char kSpeedreaderStylesheet[] = "speedreader-stylesheet";
std::string WrapStylesheetWithCSP(const std::string& stylesheet,
const std::string& atkinson,
const std::string& open_dyslexic) {
auto get_sha256 = [](const std::string& v) {
const std::string& style_hash = crypto::SHA256HashString(v);
return base::Base64Encode(base::as_bytes(base::make_span(style_hash)));
};

constexpr const char kCSP[] = R"html(
<meta name="referrer" content="no-referrer">
<meta http-equiv="Content-Security-Policy"
content="script-src 'none';
style-src-elem 'sha256-%s' 'sha256-%s' 'sha256-%s'"
content="default-src 'none';
script-src 'none';
img-src *;
font-src 'none';
base-uri 'none';
form-action 'none';
upgrade-insecure-requests;"
>)html";

return base::StrCat(
{base::StringPrintf(kCSP, get_sha256(stylesheet).c_str(),
get_sha256(atkinson).c_str(),
get_sha256(open_dyslexic).c_str()),
"<style id=\"brave_speedreader_style\">", stylesheet, "</style>",
"<style id=\"atkinson_hyperligible_font\">", atkinson, "</style>",
"<style id=\"open_dyslexic_font\">", open_dyslexic, "</style>"});
const auto make_style_data = [](std::string_view id, std::string_view data) {
const std::string& hash = crypto::SHA256HashString(data);
const std::string& sha256 =
base::Base64Encode(base::as_bytes(base::make_span(hash)));

return base::StrCat({"<script type=\"brave-style-data\" id=\"", id,
diracdeltas marked this conversation as resolved.
Show resolved Hide resolved
"\" integrity=\"", sha256, "\">", data, "</script>"});
};

return base::StrCat({kCSP,
make_style_data("brave_speedreader_style", stylesheet),
make_style_data("atkinson_hyperligible_font", atkinson),
make_style_data("open_dyslexic_font", open_dyslexic)});
}

std::string GetDistilledPageStylesheet(const base::FilePath& stylesheet_path) {
Expand Down
48 changes: 48 additions & 0 deletions test/data/speedreader/article/csp_html.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@

<!DOCTYPE html>
<head>
<meta charset="utf-8" />
<link rel="canonical" href="https://www.eff.org/deeplinks/2010/06/encrypt-web-https-everywhere-firefox-extension" />
<meta name="referrer" content="no-referrer">
<meta http-equiv="Content-Security-Policy" content="img-src 'none';" />
<title>Encrypt the Web with the HTTPS Everywhere Firefox Extension | Electronic Frontier Foundation</title>
</head>
<body>
<div id="main-content">
<div class="main-column">
<div class="panel-pane pane-page-title">
<h1>Encrypt the Web with the HTTPS Everywhere Firefox Extension</h1>
</div>
<div class="field__item even">
<p>This page is copied from https://www.eff.org/deeplinks/2010/06/encrypt-web-https-everywhere-firefox-extension, with modification. Today EFF and the Tor Project are launching a public beta of a new Firefox extension called
<a href="https://eff.org/https-everywhere">HTTPS Everywhere</a>.</p>
<p><img
src="https://a.test/should_fail.png" border="0" alt="This image is not loaded"
name="click here to encrypt the web" id="click here to encrypt the web" /></p>
<p>This Firefox extension was inspired by the launch of Google's <a
href="https://www.eff.org/deeplinks/2010/05/google-launches-encrypted-search">encrypted
search option</a>. We wanted a way to ensure that every search our browsers sent was
encrypted. At the same time, we were also able to encrypt most or all of the browser's
communications with some other sites:</p>
<ul>
<li>Google Search</li>
<li>Wikipedia</li>
<li>Twitter and Identi.ca</li>
<li>Facebook</li>
<li>EFF and Tor</li>
<li>Ixquick, DuckDuckGo, Scroogle and other small search engines</li>
<li>and lots more!</li>
</ul>
<p>Firefox users can install HTTPS Everywhere by following <a
href="https://www.eff.org/files/https-everywhere-latest.xpi">this link</a>. As always,
even if you're at an HTTPS page, remember that unless Firefox displays a colored address bar
and an unbroken lock icon in the bottom-right corner, the page is not completely encrypted
and you may still be vulnerable to various forms of eavesdropping or hacking (in many cases,
HTTPS Everywhere can't prevent this because sites incorporate insecure third-party content).
</p>
</div>
</div>
</div>
</div>
</body>
</html>
46 changes: 46 additions & 0 deletions test/data/speedreader/article/csp_http.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

<!DOCTYPE html>
<head>
<meta charset="utf-8" />
<link rel="canonical" href="https://www.eff.org/deeplinks/2010/06/encrypt-web-https-everywhere-firefox-extension" />
<title>Encrypt the Web with the HTTPS Everywhere Firefox Extension | Electronic Frontier Foundation</title>
</head>
<body>
<div id="main-content">
<div class="main-column">
<div class="panel-pane pane-page-title">
<h1>Encrypt the Web with the HTTPS Everywhere Firefox Extension</h1>
</div>
<div class="field__item even">
<p>This page is copied from https://www.eff.org/deeplinks/2010/06/encrypt-web-https-everywhere-firefox-extension, with modification. Today EFF and the Tor Project are launching a public beta of a new Firefox extension called
<a href="https://eff.org/https-everywhere">HTTPS Everywhere</a>.</p>
<p><img
src="https://a.test/should_fail.png" border="0" alt="This image is not loaded"
name="click here to encrypt the web" id="click here to encrypt the web" /></p>
<p>This Firefox extension was inspired by the launch of Google's <a
href="https://www.eff.org/deeplinks/2010/05/google-launches-encrypted-search">encrypted
search option</a>. We wanted a way to ensure that every search our browsers sent was
encrypted. At the same time, we were also able to encrypt most or all of the browser's
communications with some other sites:</p>
<ul>
<li>Google Search</li>
<li>Wikipedia</li>
<li>Twitter and Identi.ca</li>
<li>Facebook</li>
<li>EFF and Tor</li>
<li>Ixquick, DuckDuckGo, Scroogle and other small search engines</li>
<li>and lots more!</li>
</ul>
<p>Firefox users can install HTTPS Everywhere by following <a
href="https://www.eff.org/files/https-everywhere-latest.xpi">this link</a>. As always,
even if you're at an HTTPS page, remember that unless Firefox displays a colored address bar
and an unbroken lock icon in the bottom-right corner, the page is not completely encrypted
and you may still be vulnerable to various forms of eavesdropping or hacking (in many cases,
HTTPS Everywhere can't prevent this because sites incorporate insecure third-party content).
</p>
</div>
</div>
</div>
</div>
</body>
</html>
4 changes: 4 additions & 0 deletions test/data/speedreader/article/csp_http.html.mock-http-headers
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
HTTP/1.1 200 OK
Content-Type: text/html;
Content-Security-Policy: default-src 'none';img-src 'none';style-src 'none';frame-ancestors 'none';form-action 'none';sandbox

Loading
Loading