Skip to content

Commit 9dbfaec

Browse files
authored
Merge pull request #3224 from spinframework/validate-allowed-hosts-default-vars
Validate allowed_outbound_hosts template with defaults
2 parents 04b94ba + 37f3be9 commit 9dbfaec

File tree

7 files changed

+60
-13
lines changed

7 files changed

+60
-13
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/expressions/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ impl Resolver {
150150
}
151151

152152
/// Resolves the given template.
153-
fn resolve_template(&self, template: &Template) -> Result<String> {
153+
pub fn resolve_template(&self, template: &Template) -> Result<String> {
154154
let mut resolved_parts: Vec<Cow<str>> = Vec::with_capacity(template.parts().len());
155155
for part in template.parts() {
156156
resolved_parts.push(match part {

crates/loader/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ serde = { workspace = true }
1616
serde_json = { workspace = true }
1717
sha2 = { workspace = true }
1818
spin-common = { path = "../common" }
19+
spin-expressions = { path = "../expressions" }
1920
spin-locked-app = { path = "../locked-app" }
2021
spin-manifest = { path = "../manifest" }
2122
spin-outbound-networking-config = { path = "../outbound-networking-config" }

crates/loader/src/local.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use anyhow::{anyhow, bail, ensure, Context, Result};
44
use futures::{future::try_join_all, StreamExt};
55
use reqwest::Url;
66
use spin_common::{paths::parent_dir, sloth, ui::quoted_path};
7+
use spin_expressions::Resolver;
78
use spin_locked_app::{
89
locked::{
910
self, ContentPath, ContentRef, LockedApp, LockedComponent, LockedComponentDependency,
@@ -87,7 +88,8 @@ impl LocalLoader {
8788
let variables = variables
8889
.into_iter()
8990
.map(|(name, v)| Ok((name.to_string(), locked_variable(v)?)))
90-
.collect::<Result<_>>()?;
91+
.collect::<Result<BTreeMap<_, _>>>()?;
92+
let resolver = Resolver::new(variables.clone())?;
9193

9294
let triggers = triggers
9395
.into_iter()
@@ -102,10 +104,13 @@ impl LocalLoader {
102104
let sloth_guard = warn_if_component_load_slothful();
103105

104106
// Load all components concurrently
105-
let components = try_join_all(components.into_iter().map(|(id, c)| async move {
106-
self.load_component(&id, c)
107-
.await
108-
.with_context(|| format!("Failed to load component `{id}`"))
107+
let components = try_join_all(components.into_iter().map(|(id, c)| {
108+
let resolver = &resolver;
109+
async move {
110+
self.load_component(&id, c, resolver)
111+
.await
112+
.with_context(|| format!("Failed to load component `{id}`"))
113+
}
109114
}))
110115
.await?;
111116

@@ -138,11 +143,12 @@ impl LocalLoader {
138143
&self,
139144
id: &KebabId,
140145
component: v2::Component,
146+
resolver: &Resolver,
141147
) -> Result<LockedComponent> {
142148
let allowed_outbound_hosts = component
143149
.normalized_allowed_outbound_hosts()
144150
.context("`allowed_http_hosts` is malformed")?;
145-
AllowedHostsConfig::validate(&allowed_outbound_hosts)
151+
AllowedHostsConfig::validate(&allowed_outbound_hosts, resolver)
146152
.context("`allowed_outbound_hosts` is malformed")?;
147153

148154
let component_requires_service_chaining = requires_service_chaining(&component);
@@ -872,8 +878,7 @@ mod test {
872878
let err_ctx = format!("{err:#}");
873879
assert!(
874880
err_ctx.contains(r#""/" is not a valid destination file name"#),
875-
"expected error to show destination file name but got {}",
876-
err_ctx
881+
"expected error to show destination file name but got {err_ctx}",
877882
);
878883
Ok(())
879884
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Failed to load Spin app from "<test-dir>/invalid-variable-default-in-allowed-hosts.toml"
2+
3+
Caused by:
4+
0: Failed to load component `test`
5+
1: `allowed_outbound_hosts` is malformed
6+
2: using default variable value(s) with template "https://{{ invalid_default_host }}" results in invalid config "https://invalid host"
7+
3: Invalid allowed host "invalid host"
8+
4: invalid international domain name
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
spin_manifest_version = 2
2+
3+
[application]
4+
name = "invalid-app"
5+
6+
[variables]
7+
invalid_default_host = { default = "invalid host" }
8+
9+
[[trigger.fake]]
10+
component = "test"
11+
12+
[component.test]
13+
source = "dummy.wasm"
14+
allowed_outbound_hosts = ["https://{{ invalid_default_host }}"]

crates/outbound-networking-config/src/allowed_hosts.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::sync::Arc;
33

44
use anyhow::{bail, ensure, Context as _};
55
use futures_util::future::{BoxFuture, Shared};
6+
use spin_expressions::Resolver;
67
use url::Host;
78

89
/// The domain used for service chaining.
@@ -433,6 +434,22 @@ impl PartialAllowedHostConfig {
433434
Self::Unresolved(t) => AllowedHostConfig::parse(resolver.resolve_template(&t)?),
434435
}
435436
}
437+
438+
/// Validates this config. Only templates that can be resolved with default
439+
/// values from the given resolver will be fully validated.
440+
fn validate(&self, resolver: &Resolver) -> anyhow::Result<()> {
441+
if let Self::Unresolved(template) = self {
442+
let Ok(resolved) = resolver.resolve_template(template) else {
443+
// We're missing a default value so we can't validate further
444+
return Ok(());
445+
};
446+
AllowedHostConfig::parse(&resolved).with_context(|| {
447+
let template_str = template.to_string();
448+
format!("using default variable value(s) with template {template_str:?} results in invalid config {resolved:?}")
449+
})?;
450+
}
451+
Ok(())
452+
}
436453
}
437454

438455
/// Represents an allowed_outbound_hosts config.
@@ -457,10 +474,11 @@ impl AllowedHostsConfig {
457474
Ok(Self::SpecificHosts(allowed))
458475
}
459476

460-
/// Validate the given allowed_outbound_hosts values. Templated values are
461-
/// only validated against template syntax.
462-
pub fn validate<S: AsRef<str>>(hosts: &[S]) -> anyhow::Result<()> {
463-
_ = Self::parse_partial(hosts)?;
477+
/// Validates the given allowed_outbound_hosts values with the given resolver.
478+
pub fn validate<S: AsRef<str>>(hosts: &[S], resolver: &Resolver) -> anyhow::Result<()> {
479+
for partial in Self::parse_partial(hosts)? {
480+
partial.validate(resolver)?;
481+
}
464482
Ok(())
465483
}
466484

0 commit comments

Comments
 (0)