Skip to content
Open
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
4 changes: 3 additions & 1 deletion .config/spellcheck.dic
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
86
88
Monterey
IC
ICP
Expand Down Expand Up @@ -85,3 +85,5 @@ DoS
Ctrl
Cmd
Banxa
Subresource
stylesheet
2 changes: 2 additions & 0 deletions CHANGELOG-Sns_Aggregator.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ The SNS Aggregator is released through proposals in the Network Nervous System.
### Fixed
### Security

- Pin the home page's Prism script and stylesheet with Subresource Integrity and load them from the certified gateway.

## [Proposal 137283](https://dashboard.internetcomputer.org/proposal/137283)
### Added
- Include SNS Governance metrics.
Expand Down
3 changes: 3 additions & 0 deletions rs/sns_aggregator/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ use serde_bytes::ByteBuf;
use sha2::{Digest, Sha256};
use std::collections::HashMap;

#[cfg(test)]
mod tests;

/// A standard HTTP header
type HeaderField = (String, String);

Expand Down
58 changes: 58 additions & 0 deletions rs/sns_aggregator/src/assets/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//! Tests for the aggregator assets
#![allow(clippy::panic)]
#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]

/// The home page, exactly as it is compiled into the canister.
const HOME_PAGE: &str = include_str!("../index.html");

/// The text of every tag in the given HTML, without the enclosing angle brackets.
///
/// For example `<link href="x" />` yields `link href="x" /`.
fn tag_bodies(html: &str) -> Vec<&str> {
html.split('<')
.skip(1)
.filter_map(|chunk| chunk.split_once('>').map(|(body, _rest)| body))
.collect()
}

/// True if the tag body is a tag with the given name, such as `script` or `link`.
fn has_tag_name(tag_body: &str, name: &str) -> bool {
tag_body
.strip_prefix(name)
.is_some_and(|rest| rest.is_empty() || rest.starts_with('/') || rest.starts_with(char::is_whitespace))
}

/// Every external `<script>` and `<link>` on the home page must be pinned with a
/// Subresource Integrity hash and must come from a certified gateway.
///
/// The hashes themselves are verified by hand, because the test has no network.
#[test]
fn home_page_pins_every_external_script_and_stylesheet() {
let mut pinned_tags = 0;
for tag_body in tag_bodies(HOME_PAGE) {
if !has_tag_name(tag_body, "script") && !has_tag_name(tag_body, "link") {
continue;
}
if !tag_body.contains("src=\"https://") && !tag_body.contains("href=\"https://") {
continue;
}
assert!(
tag_body.contains("integrity=\"sha384-"),
"External resource has no Subresource Integrity hash: <{tag_body}>"
);
assert!(
tag_body.contains("crossorigin=\"anonymous\""),
"External resource has no crossorigin=\"anonymous\", so the browser ignores its Subresource Integrity hash: <{tag_body}>"
);
assert!(
!tag_body.contains(".raw."),
"External resource is loaded from an uncertified raw gateway: <{tag_body}>"
);
pinned_tags += 1;
}
assert!(
pinned_tags >= 2,
"The scan found {pinned_tags} external resources on the home page but expected at least 2, so the scan is broken."
);
}
20 changes: 17 additions & 3 deletions rs/sns_aggregator/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@
<meta charset="utf-8" />
<title>SNS aggregator</title>

<!-- Juno's CDN -->
<!--
Juno's CDN.
The integrity attribute pins the exact bytes of each file.
If you change a version in a URL, compute the new hash with:
curl -s URL | openssl dgst -sha384 -binary | openssl base64 -A
If the hash is wrong, the browser refuses the file.
The page still works, but the code examples lose their colors.
-->
<link
href="https://fmkjf-bqaaa-aaaal-acpza-cai.raw.icp0.io/libs/prism-themes/1.9.0/themes/prism-vsc-dark-plus.min.css"
href="https://fmkjf-bqaaa-aaaal-acpza-cai.icp0.io/libs/prism-themes/1.9.0/themes/prism-vsc-dark-plus.min.css"
rel="stylesheet"
integrity="sha384-UavePWn2zyHuZbvVQRu5n4XEhCiueqts8YR0Dqq1mnsearU03Jfxl1XSHffMI1kq"
crossorigin="anonymous"
/>

<style>
Expand Down Expand Up @@ -560,6 +569,11 @@ <h2>
document.addEventListener("DOMContentLoaded", loadSnses, { once: true });
</script>

<script src="https://fmkjf-bqaaa-aaaal-acpza-cai.raw.icp0.io/libs/prismjs/1.29.0/prism.min.js"></script>
<!-- Juno's CDN. To change the version, read the note in the head. -->
<script
src="https://fmkjf-bqaaa-aaaal-acpza-cai.icp0.io/libs/prismjs/1.29.0/prism.min.js"
integrity="sha384-06z5D//U/xpvxZHuUz92xBvq3DqBBFi7Up53HRrbV7Jlv7Yvh/MZ7oenfUe9iCEt"
crossorigin="anonymous"
></script>
</body>
</html>
Loading