Skip to content

Commit

Permalink
feat(web-security): implement ab API for importing content security p…
Browse files Browse the repository at this point in the history
…olicies (CSP)

Previously, it was possible to construct content security policies (CSP) templates through a UI or
high-level API only. This approach works well when the user wants to create a new policy template
from scratch. However, there are times when user would prefer to base their policy on or "inherit"
it from an existing policy. This change adds an API to allow importing existing content security
policies, either by proving full policy value directly or pointing to an arbitrary web page to
import policy from.
  • Loading branch information
azasypkin committed Oct 13, 2023
1 parent 2c6a1ee commit 2db6c0a
Show file tree
Hide file tree
Showing 21 changed files with 2,033 additions and 101 deletions.
121 changes: 121 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ async-stream = "0.3.5"
bytes = "1.5.0"
chrono = { version = "0.4.31", default-features = false }
clap = "4.4.6"
content-security-policy = "0.5.1"
cron = "0.12.0"
directories = "5.0.1"
dotenvy = "0.15.7"
env_logger = "0.10.0"
futures = "0.3.28"
handlebars = "5.0.0-beta.5"
hex = "0.4.3"
html5ever = "0.26.0"
humantime = "2.1.0"
itertools = "0.11.0"
lettre = { version = "0.10.4", default-features = false }
Expand Down Expand Up @@ -74,6 +76,7 @@ default = [
"actix-web/secure-cookies",
"bytes/serde",
"clap/env",
"content-security-policy/serde",
"handlebars/rust-embed",
"insta/filters",
"insta/json",
Expand Down
87 changes: 84 additions & 3 deletions src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ pub use self::{
email_transport::{EmailTransport, EmailTransportError},
ip_addr_ext::IpAddrExt,
};

#[cfg(test)]
pub use self::dns_resolver::tests;
use url::Url;

/// Network utilities.
#[derive(Clone)]
Expand All @@ -26,4 +24,87 @@ impl<DR: DnsResolver, ET: EmailTransport> Network<DR, ET> {
email_transport,
}
}

/// Checks if provided URL is a publicly accessible web URL.
pub async fn is_public_web_url(&self, url: &Url) -> bool {
if url.scheme() != "http" && url.scheme() != "https" {
return false;
}

// Checks if the specific hostname is a domain and public (not pointing to the local network).
if let Some(domain) = url.domain() {
match self.resolver.lookup_ip(domain).await {
Ok(lookup) => lookup.iter().all(|ip| IpAddrExt::is_global(&ip)),
Err(err) => {
log::error!("Cannot resolve domain ({domain}) to IP: {err}");
false
}
}
} else {
false
}
}
}

#[cfg(test)]
pub mod tests {
use super::Network;
use lettre::transport::stub::AsyncStubTransport;
use std::net::Ipv4Addr;
use trust_dns_resolver::{
error::{ResolveError, ResolveErrorKind},
proto::rr::{rdata::A, RData, Record},
Name,
};
use url::Url;

pub use super::dns_resolver::tests::*;

#[actix_rt::test]
async fn correctly_checks_public_web_urls() -> anyhow::Result<()> {
let public_network = Network::new(
MockResolver::new_with_records::<1>(vec![Record::from_rdata(
Name::new(),
300,
RData::A(A(Ipv4Addr::new(172, 32, 0, 2))),
)]),
AsyncStubTransport::new_ok(),
);

// Only `http` and `https` should be supported.
for (protocol, is_supported) in [
("ftp", false),
("wss", false),
("http", true),
("https", true),
] {
let url = Url::parse(&format!("{}://secutils.dev/my-page", protocol))?;
assert_eq!(public_network.is_public_web_url(&url).await, is_supported);
}

// Hosts that resolve to local IPs aren't supported.
let url = Url::parse("https://secutils.dev/my-page")?;
let local_network = Network::new(
MockResolver::new_with_records::<1>(vec![Record::from_rdata(
Name::new(),
300,
RData::A(A(Ipv4Addr::new(127, 0, 0, 1))),
)]),
AsyncStubTransport::new_ok(),
);
for (network, is_supported) in [(public_network, true), (local_network, false)] {
assert_eq!(network.is_public_web_url(&url).await, is_supported);
}

// Hosts that fail to resolve aren't supported and gracefully handled.
let broken_network = Network::new(
MockResolver::new_with_error(ResolveError::from(ResolveErrorKind::Message(
"can not lookup IPs",
))),
AsyncStubTransport::new_ok(),
);
assert!(!broken_network.is_public_web_url(&url).await);

Ok(())
}
}
24 changes: 19 additions & 5 deletions src/network/dns_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,29 +50,43 @@ pub mod tests {
#[derive(Clone)]
pub struct MockResolver<const N: usize = 0> {
records: [Record; N],
error: Option<ResolveError>,
}

impl<const N: usize> DnsResolver for MockResolver<N> {
fn lookup_ip<'a>(&'a self, _: &'a str) -> BoxFuture<'a, Result<LookupIp, ResolveError>> {
Box::pin(futures::future::ready(Ok(LookupIp::from(
Lookup::new_with_max_ttl(
Box::pin(futures::future::ready(if let Some(err) = &self.error {
Err(err.clone())
} else {
Ok(LookupIp::from(Lookup::new_with_max_ttl(
Query::query(Name::new(), RecordType::A),
Arc::new(self.records.clone()),
),
))))
)))
}))
}
}

impl MockResolver {
pub fn new() -> Self {
MockResolver { records: [] }
MockResolver {
records: [],
error: None,
}
}
}

impl MockResolver {
pub fn new_with_records<const N: usize>(records: Vec<Record>) -> MockResolver<N> {
MockResolver {
records: records.try_into().unwrap(),
error: None,
}
}

pub fn new_with_error(err: ResolveError) -> MockResolver<0> {
MockResolver {
records: [],
error: Some(err),
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ pub use self::{
WebScraperResourcesResponse,
},
web_security::{
ContentSecurityPolicy, ContentSecurityPolicyDirective,
ContentSecurityPolicy, ContentSecurityPolicyDirective, ContentSecurityPolicyImportType,
ContentSecurityPolicyRequireTrustedTypesForDirectiveValue,
ContentSecurityPolicySandboxDirectiveValue, ContentSecurityPolicySource,
ContentSecurityPolicyWebrtcDirectiveValue, UtilsWebSecurityAction,
UtilsWebSecurityActionResult,
ContentSecurityPolicyTrustedTypesDirectiveValue, ContentSecurityPolicyWebrtcDirectiveValue,
UtilsWebSecurityAction, UtilsWebSecurityActionResult,
},
webhooks::{
AutoResponder, AutoResponderMethod, AutoResponderRequest, UtilsWebhooksAction,
Expand Down
Loading

0 comments on commit 2db6c0a

Please sign in to comment.