Skip to content

Commit 36d04ec

Browse files
committed
Add address check for mysql
Signed-off-by: Ryan Levick <[email protected]>
1 parent ed4e13f commit 36d04ec

File tree

10 files changed

+83
-32
lines changed

10 files changed

+83
-32
lines changed

Cargo.lock

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/outbound-mysql/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ mysql_async = { version = "0.32.2", default-features = false, features = [
1616
] }
1717
# Removing default features for mysql_common to remove flate2/zlib feature
1818
mysql_common = { version = "0.30.6", default-features = false }
19+
spin-app = { path = "../app" }
1920
spin-core = { path = "../core" }
21+
spin-outbound-networking = { path = "../outbound-networking" }
2022
spin-world = { path = "../world" }
2123
table = { path = "../table" }
2224
tokio = { version = "1", features = ["rt-multi-thread"] }

crates/outbound-mysql/src/lib.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
use anyhow::Result;
1+
use anyhow::{Context, Result};
22
use mysql_async::{consts::ColumnType, from_value_opt, prelude::*, Opts, OptsBuilder, SslOpts};
3+
use spin_app::DynamicHostComponent;
34
use spin_core::wasmtime::component::Resource;
45
use spin_core::{async_trait, HostComponent};
56
use spin_world::v1::mysql as v1;
@@ -12,6 +13,7 @@ use url::Url;
1213
/// A simple implementation to support outbound mysql connection
1314
#[derive(Default)]
1415
pub struct OutboundMysql {
16+
allowed_hosts: Option<spin_outbound_networking::AllowedHosts>,
1517
pub connections: table::Table<mysql_async::Conn>,
1618
}
1719

@@ -24,6 +26,10 @@ impl OutboundMysql {
2426
.get_mut(connection.rep())
2527
.ok_or_else(|| v2::Error::ConnectionFailed("no connection found".into()))
2628
}
29+
30+
fn is_address_allowed(&self, address: &str, default: bool) -> bool {
31+
spin_outbound_networking::check_address(address, "mysql", &self.allowed_hosts, default)
32+
}
2733
}
2834

2935
impl HostComponent for OutboundMysql {
@@ -42,11 +48,33 @@ impl HostComponent for OutboundMysql {
4248
}
4349
}
4450

51+
impl DynamicHostComponent for OutboundMysql {
52+
fn update_data(
53+
&self,
54+
data: &mut Self::Data,
55+
component: &spin_app::AppComponent,
56+
) -> anyhow::Result<()> {
57+
let hosts = component
58+
.get_metadata(spin_outbound_networking::ALLOWED_HOSTS_KEY)?
59+
.unwrap_or_default();
60+
data.allowed_hosts = hosts
61+
.map(|h| spin_outbound_networking::AllowedHosts::parse(&h[..]))
62+
.transpose()
63+
.context("`allowed_outbound_hosts` contained an invalid url")?;
64+
Ok(())
65+
}
66+
}
67+
4568
impl v2::Host for OutboundMysql {}
4669

4770
#[async_trait]
4871
impl v2::HostConnection for OutboundMysql {
4972
async fn open(&mut self, address: String) -> Result<Result<Resource<Connection>, v2::Error>> {
73+
if !self.is_address_allowed(&address, false) {
74+
return Ok(Err(v2::Error::ConnectionFailed(format!(
75+
"address {address} is not permitted"
76+
))));
77+
}
5078
Ok(async {
5179
self.connections
5280
.push(
@@ -125,6 +153,11 @@ impl v2::HostConnection for OutboundMysql {
125153
/// Delegate a function call to the v2::HostConnection implementation
126154
macro_rules! delegate {
127155
($self:ident.$name:ident($address:expr, $($arg:expr),*)) => {{
156+
if !$self.is_address_allowed(&$address, true) {
157+
return Ok(Err(v1::MysqlError::ConnectionFailed(format!(
158+
"address {} is not permitted", $address
159+
))));
160+
}
128161
let connection = match <Self as v2::HostConnection>::open($self, $address).await? {
129162
Ok(c) => c,
130163
Err(e) => return Ok(Err(e.into())),

crates/outbound-networking/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ edition.workspace = true
66

77
[dependencies]
88
anyhow = "1.0"
9+
spin-locked-app = { path = "../locked-app" }
10+
terminal = { path = "../terminal" }
911
url = "2.4.1"

crates/outbound-networking/src/lib.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,40 @@
11
use anyhow::Context;
2+
use spin_locked_app::MetadataKey;
23
use url::Url;
34

5+
pub const ALLOWED_HOSTS_KEY: MetadataKey<Option<Vec<String>>> =
6+
MetadataKey::new("allowed_outbound_hosts");
7+
8+
/// Checks address against allowed hosts
9+
///
10+
/// Emits several warnings
11+
pub fn check_address(
12+
address: &str,
13+
scheme: &str,
14+
allowed_hosts: &Option<AllowedHosts>,
15+
default: bool,
16+
) -> bool {
17+
let Ok(url) = parse_url_with_host(address, scheme) else {
18+
terminal::warn!(
19+
"A component tried to make a request to an address that could not be parsed as a url {address:?}."
20+
);
21+
return false;
22+
};
23+
let is_allowed = if let Some(allowed_hosts) = allowed_hosts {
24+
allowed_hosts.allows(url.clone())
25+
} else {
26+
default
27+
};
28+
29+
if !is_allowed {
30+
terminal::warn!("A component tried to make a request to non-allowed address {address:?}.");
31+
if let (Some(host), Some(port)) = (url.host_str(), url.port_or_known_default()) {
32+
eprintln!("To allow requests, add 'allowed_outbound_hosts = '[\"{host}:{port}\"]' to the manifest component section.");
33+
}
34+
}
35+
is_allowed
36+
}
37+
438
/// Try to parse the url that may or not include the provided scheme.
539
///
640
/// If the parsing fails, the url is appended with the scheme and parsing

crates/outbound-redis/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@ anyhow = "1.0"
1212
redis = { version = "0.21", features = ["tokio-comp", "tokio-native-tls-comp"] }
1313
spin-app = { path = "../app" }
1414
spin-core = { path = "../core" }
15-
spin-locked-app = { path = "../locked-app" }
1615
spin-world = { path = "../world" }
1716
spin-outbound-networking = { path = "../outbound-networking" }
1817
table = { path = "../table" }
1918
tokio = { version = "1", features = ["sync"] }
2019
tracing = { workspace = true }
21-
terminal = { path = "../terminal" }

crates/outbound-redis/src/host_component.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ impl DynamicHostComponent for OutboundRedisComponent {
2828
component: &spin_app::AppComponent,
2929
) -> anyhow::Result<()> {
3030
let hosts = component
31-
.get_metadata(crate::ALLOWED_HOSTS_KEY)?
31+
.get_metadata(spin_outbound_networking::ALLOWED_HOSTS_KEY)?
3232
.unwrap_or_default();
3333
data.allowed_hosts = hosts
3434
.map(|h| spin_outbound_networking::AllowedHosts::parse(&h[..]))

crates/outbound-redis/src/lib.rs

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,11 @@ mod host_component;
33
use anyhow::Result;
44
use redis::{aio::Connection, AsyncCommands, FromRedisValue, Value};
55
use spin_core::{async_trait, wasmtime::component::Resource};
6-
use spin_locked_app::MetadataKey;
76
use spin_world::v1::redis as v1;
87
use spin_world::v2::redis::{
98
self as v2, Connection as RedisConnection, Error, RedisParameter, RedisResult,
109
};
1110

12-
pub const ALLOWED_HOSTS_KEY: MetadataKey<Option<Vec<String>>> =
13-
MetadataKey::new("allowed_outbound_hosts");
14-
1511
pub use host_component::OutboundRedisComponent;
1612

1713
struct RedisResults(Vec<RedisResult>);
@@ -50,27 +46,7 @@ impl Default for OutboundRedis {
5046

5147
impl OutboundRedis {
5248
fn is_address_allowed(&self, address: &str, default: bool) -> bool {
53-
let Ok(url) = spin_outbound_networking::parse_url_with_host(address, "redis") else {
54-
terminal::warn!(
55-
"A component tried to make a request to an address that could not be parsed as a url {address:?}."
56-
);
57-
return false;
58-
};
59-
let is_allowed = if let Some(allowed_hosts) = &self.allowed_hosts {
60-
allowed_hosts.allows(url.clone())
61-
} else {
62-
default
63-
};
64-
65-
if !is_allowed {
66-
terminal::warn!(
67-
"A component tried to make a request to non-allowed address {address:?}."
68-
);
69-
if let (Some(host), Some(port)) = (url.host_str(), url.port_or_known_default()) {
70-
eprintln!("To allow requests, add 'allowed_outbound_hosts = '[\"{host}:{port}\"]' to the manifest component section.");
71-
}
72-
}
73-
is_allowed
49+
spin_outbound_networking::check_address(address, "redis", &self.allowed_hosts, default)
7450
}
7551

7652
async fn establish_connection(

crates/trigger/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,14 @@ impl<Executor: TriggerExecutor> TriggerExecutorBuilder<Executor> {
113113
if !self.disable_default_host_components {
114114
builder.link_import(|l, _| wasmtime_wasi_http::proxy::add_to_linker(l))?;
115115
builder.add_host_component(outbound_pg::OutboundPg::default())?;
116-
builder.add_host_component(outbound_mysql::OutboundMysql::default())?;
117116
self.loader.add_dynamic_host_component(
118117
&mut builder,
119118
outbound_redis::OutboundRedisComponent,
120119
)?;
120+
self.loader.add_dynamic_host_component(
121+
&mut builder,
122+
outbound_mysql::OutboundMysql::default(),
123+
)?;
121124
self.loader.add_dynamic_host_component(
122125
&mut builder,
123126
runtime_config::llm::build_component(&runtime_config, init_data.llm.use_gpu)

examples/rust-outbound-mysql/spin.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ component = "rust-outbound-mysql"
1313
[component.rust-outbound-mysql]
1414
environment = { DB_URL = "mysql://spin:[email protected]/spin_dev" }
1515
source = "target/wasm32-wasi/release/rust_outbound_mysql.wasm"
16+
allowed_outbound_hosts = ["127.0.0.1:3306"]
1617
[component.rust-outbound-mysql.build]
1718
command = "cargo build --target wasm32-wasi --release"

0 commit comments

Comments
 (0)