Skip to content

Commit 6e4c340

Browse files
feat: adds support to non-configurable caching on cache-audit (#345)
Co-authored-by: William Woodruff <[email protected]>
1 parent db594e2 commit 6e4c340

File tree

4 files changed

+156
-71
lines changed

4 files changed

+156
-71
lines changed

Diff for: src/audit/cache_poisoning.rs

+109-71
Original file line numberDiff line numberDiff line change
@@ -10,103 +10,126 @@ use github_actions_models::workflow::Trigger;
1010
use std::ops::Deref;
1111
use std::sync::LazyLock;
1212

13+
/// The value type that controls the activation/deactivation of caching
1314
#[derive(PartialEq)]
14-
enum ControlValue {
15+
enum CacheControlValue {
1516
Boolean,
1617
String,
1718
}
1819

19-
enum CacheControl {
20+
/// The input that controls the behaviour of a configurable caching Action
21+
enum CacheControlInput {
22+
/// Opt-in means that cache becomes **enabled** when the control value matches.
2023
OptIn(&'static str),
24+
/// Opt-out means that cache becomes **disabled** when the control value matches.
2125
OptOut(&'static str),
2226
}
2327

2428
/// The general schema for a cache-aware actions
25-
struct CacheAwareAction<'w> {
29+
struct ControllableCacheAction<'w> {
2630
/// The owner/repo part within the Action full coordinate
2731
uses: Uses<'w>,
2832
/// The input that controls caching behavior
29-
cache_control: CacheControl,
33+
control_input: CacheControlInput,
3034
/// The type of value used to opt-in/opt-out (Boolean, String)
31-
control_value: ControlValue,
35+
control_value: CacheControlValue,
3236
/// Whether this Action adopts caching as the default behavior
3337
caching_by_default: bool,
3438
}
3539

40+
enum CacheAwareAction<'w> {
41+
Configurable(ControllableCacheAction<'w>),
42+
NotConfigurable(Uses<'w>),
43+
}
44+
45+
impl CacheAwareAction<'_> {
46+
fn uses(&self) -> Uses {
47+
match self {
48+
CacheAwareAction::Configurable(inner) => inner.uses,
49+
CacheAwareAction::NotConfigurable(inner) => *inner,
50+
}
51+
}
52+
}
53+
3654
/// The list of know cache-aware actions
3755
/// In the future we can easily retrieve this list from the static API,
3856
/// since it should be easily serializable
3957
static KNOWN_CACHE_AWARE_ACTIONS: LazyLock<Vec<CacheAwareAction>> = LazyLock::new(|| {
4058
vec![
4159
// https://github.com/actions/cache/blob/main/action.yml
42-
CacheAwareAction {
60+
CacheAwareAction::Configurable(ControllableCacheAction {
4361
uses: Uses::from_step("actions/cache").unwrap(),
44-
cache_control: CacheControl::OptOut("lookup-only"),
45-
control_value: ControlValue::Boolean,
62+
control_input: CacheControlInput::OptOut("lookup-only"),
63+
control_value: CacheControlValue::Boolean,
4664
caching_by_default: true,
47-
},
48-
CacheAwareAction {
65+
}),
66+
// https://github.com/actions/setup-java/blob/main/action.yml
67+
CacheAwareAction::Configurable(ControllableCacheAction {
4968
uses: Uses::from_step("actions/setup-java").unwrap(),
50-
cache_control: CacheControl::OptIn("cache"),
51-
control_value: ControlValue::String,
69+
control_input: CacheControlInput::OptIn("cache"),
70+
control_value: CacheControlValue::String,
5271
caching_by_default: false,
53-
},
72+
}),
5473
// https://github.com/actions/setup-go/blob/main/action.yml
55-
CacheAwareAction {
74+
CacheAwareAction::Configurable(ControllableCacheAction {
5675
uses: Uses::from_step("actions/setup-go").unwrap(),
57-
cache_control: CacheControl::OptIn("cache"),
58-
control_value: ControlValue::Boolean,
76+
control_input: CacheControlInput::OptIn("cache"),
77+
control_value: CacheControlValue::Boolean,
5978
caching_by_default: true,
60-
},
79+
}),
6180
// https://github.com/actions/setup-node/blob/main/action.yml
62-
CacheAwareAction {
81+
CacheAwareAction::Configurable(ControllableCacheAction {
6382
uses: Uses::from_step("actions/setup-node").unwrap(),
64-
cache_control: CacheControl::OptIn("cache"),
65-
control_value: ControlValue::String,
83+
control_input: CacheControlInput::OptIn("cache"),
84+
control_value: CacheControlValue::String,
6685
caching_by_default: false,
67-
},
86+
}),
6887
// https://github.com/actions/setup-python/blob/main/action.yml
69-
CacheAwareAction {
88+
CacheAwareAction::Configurable(ControllableCacheAction {
7089
uses: Uses::from_step("actions/setup-python").unwrap(),
71-
cache_control: CacheControl::OptIn("cache"),
72-
control_value: ControlValue::String,
90+
control_input: CacheControlInput::OptIn("cache"),
91+
control_value: CacheControlValue::String,
7392
caching_by_default: false,
74-
},
93+
}),
7594
// https://github.com/actions/setup-dotnet/blob/main/action.yml
76-
CacheAwareAction {
95+
CacheAwareAction::Configurable(ControllableCacheAction {
7796
uses: Uses::from_step("actions/setup-dotnet").unwrap(),
78-
cache_control: CacheControl::OptIn("cache"),
79-
control_value: ControlValue::Boolean,
97+
control_input: CacheControlInput::OptIn("cache"),
98+
control_value: CacheControlValue::Boolean,
8099
caching_by_default: false,
81-
},
100+
}),
82101
// https://github.com/astral-sh/setup-uv/blob/main/action.yml
83-
CacheAwareAction {
102+
CacheAwareAction::Configurable(ControllableCacheAction {
84103
uses: Uses::from_step("astral-sh/setup-uv").unwrap(),
85-
cache_control: CacheControl::OptOut("enable-cache"),
86-
control_value: ControlValue::String,
104+
control_input: CacheControlInput::OptOut("enable-cache"),
105+
control_value: CacheControlValue::String,
87106
caching_by_default: true,
88-
},
107+
}),
89108
// https://github.com/Swatinem/rust-cache/blob/master/action.yml
90-
CacheAwareAction {
109+
CacheAwareAction::Configurable(ControllableCacheAction {
91110
uses: Uses::from_step("Swatinem/rust-cache").unwrap(),
92-
cache_control: CacheControl::OptOut("lookup-only"),
93-
control_value: ControlValue::Boolean,
111+
control_input: CacheControlInput::OptOut("lookup-only"),
112+
control_value: CacheControlValue::Boolean,
94113
caching_by_default: true,
95-
},
114+
}),
96115
// https://github.com/ruby/setup-ruby/blob/master/action.yml
97-
CacheAwareAction {
116+
CacheAwareAction::Configurable(ControllableCacheAction {
98117
uses: Uses::from_step("ruby/setup-ruby").unwrap(),
99-
cache_control: CacheControl::OptIn("bundler-cache"),
100-
control_value: ControlValue::Boolean,
118+
control_input: CacheControlInput::OptIn("bundler-cache"),
119+
control_value: CacheControlValue::Boolean,
101120
caching_by_default: false,
102-
},
121+
}),
103122
// https://github.com/PyO3/maturin-action/blob/main/action.yml
104-
CacheAwareAction {
123+
CacheAwareAction::Configurable(ControllableCacheAction {
105124
uses: Uses::from_step("PyO3/maturin-action").unwrap(),
106-
cache_control: CacheControl::OptIn("sccache"),
107-
control_value: ControlValue::Boolean,
125+
control_input: CacheControlInput::OptIn("sccache"),
126+
control_value: CacheControlValue::Boolean,
108127
caching_by_default: false,
109-
},
128+
}),
129+
// https://github.com/Mozilla-Actions/sccache-action/blob/main/action.yml
130+
CacheAwareAction::NotConfigurable(
131+
Uses::from_step("Mozilla-Actions/sccache-action").unwrap(),
132+
),
110133
]
111134
});
112135

@@ -145,6 +168,7 @@ enum CacheUsage {
145168
ConditionalOptIn,
146169
DirectOptIn,
147170
DefaultActionBehaviour,
171+
AlwaysCache,
148172
}
149173

150174
enum PublishingArtifactsScenario<'w> {
@@ -204,7 +228,7 @@ impl CachePoisoning {
204228
))
205229
}
206230

207-
fn evaluate_default_action_behaviour(action: &CacheAwareAction) -> Option<CacheUsage> {
231+
fn evaluate_default_action_behaviour(action: &ControllableCacheAction) -> Option<CacheUsage> {
208232
if action.caching_by_default {
209233
Some(CacheUsage::DefaultActionBehaviour)
210234
} else {
@@ -215,20 +239,20 @@ impl CachePoisoning {
215239
fn evaluate_user_defined_opt_in(
216240
cache_control_input: &str,
217241
env: &Env,
218-
action: &CacheAwareAction,
242+
action: &ControllableCacheAction,
219243
) -> Option<CacheUsage> {
220244
match env.get(cache_control_input) {
221245
None => None,
222246
Some(value) => match value.to_string().as_str() {
223-
"true" if matches!(action.control_value, ControlValue::Boolean) => {
247+
"true" if matches!(action.control_value, CacheControlValue::Boolean) => {
224248
Some(CacheUsage::DirectOptIn)
225249
}
226-
"false" if matches!(action.control_value, ControlValue::Boolean) => {
250+
"false" if matches!(action.control_value, CacheControlValue::Boolean) => {
227251
// Explicitly opts out from caching
228252
None
229253
}
230254
other => match ExplicitExpr::from_curly(other) {
231-
None if matches!(action.control_value, ControlValue::String) => {
255+
None if matches!(action.control_value, CacheControlValue::String) => {
232256
Some(CacheUsage::DirectOptIn)
233257
}
234258
None => None,
@@ -238,44 +262,36 @@ impl CachePoisoning {
238262
}
239263
}
240264

241-
fn evaluate_cache_usage(&self, target_step: &str, env: &Env) -> Option<CacheUsage> {
242-
let known_action = KNOWN_CACHE_AWARE_ACTIONS.iter().find(|action| {
243-
let Uses::Repository(well_known_uses) = action.uses else {
244-
return false;
245-
};
246-
247-
let Some(Uses::Repository(target_uses)) = Uses::from_step(target_step) else {
248-
return false;
249-
};
250-
251-
target_uses.matches(well_known_uses)
252-
})?;
253-
254-
let cache_control_input = env.keys().find(|k| match known_action.cache_control {
255-
CacheControl::OptIn(inner) => *k == inner,
256-
CacheControl::OptOut(inner) => *k == inner,
265+
fn usage_of_controllable_caching(
266+
&self,
267+
env: &Env,
268+
controllable: &ControllableCacheAction,
269+
) -> Option<CacheUsage> {
270+
let cache_control_input = env.keys().find(|k| match controllable.control_input {
271+
CacheControlInput::OptIn(inner) => *k == inner,
272+
CacheControlInput::OptOut(inner) => *k == inner,
257273
});
258274

259275
match cache_control_input {
260276
// when not using the specific Action input to control caching behaviour,
261277
// we evaluate whether it uses caching by default
262-
None => CachePoisoning::evaluate_default_action_behaviour(known_action),
278+
None => CachePoisoning::evaluate_default_action_behaviour(controllable),
263279

264280
// otherwise, we infer from the value assigned to the cache control input
265281
Some(key) => {
266282
// first, we extract the value assigned to that input
267283
let declared_usage =
268-
CachePoisoning::evaluate_user_defined_opt_in(key, env, known_action);
284+
CachePoisoning::evaluate_user_defined_opt_in(key, env, controllable);
269285

270286
// we now evaluate the extracted value against the opt-in semantics
271287
match &declared_usage {
272288
Some(CacheUsage::DirectOptIn) => {
273-
match known_action.cache_control {
289+
match controllable.control_input {
274290
// in this case, we just follow the opt-in
275-
CacheControl::OptIn(_) => declared_usage,
291+
CacheControlInput::OptIn(_) => declared_usage,
276292
// otherwise, the user opted for disabling the cache
277293
// hence we don't return a CacheUsage
278-
CacheControl::OptOut(_) => None,
294+
CacheControlInput::OptOut(_) => None,
279295
}
280296
}
281297
// Because we can't evaluate expressions, there is nothing to do
@@ -286,6 +302,27 @@ impl CachePoisoning {
286302
}
287303
}
288304

305+
fn evaluate_cache_usage(&self, target_step: &str, env: &Env) -> Option<CacheUsage> {
306+
let known_action = KNOWN_CACHE_AWARE_ACTIONS.iter().find(|action| {
307+
let Uses::Repository(well_known_uses) = action.uses() else {
308+
return false;
309+
};
310+
311+
let Some(Uses::Repository(target_uses)) = Uses::from_step(target_step) else {
312+
return false;
313+
};
314+
315+
target_uses.matches(well_known_uses)
316+
})?;
317+
318+
match known_action {
319+
CacheAwareAction::Configurable(action) => {
320+
self.usage_of_controllable_caching(env, action)
321+
}
322+
CacheAwareAction::NotConfigurable(_) => Some(CacheUsage::AlwaysCache),
323+
}
324+
}
325+
289326
fn uses_cache_aware_step<'w>(
290327
&self,
291328
step: &Step<'w>,
@@ -298,6 +335,7 @@ impl CachePoisoning {
298335
let cache_usage = self.evaluate_cache_usage(uses, with)?;
299336

300337
let (yaml_key, annotation) = match cache_usage {
338+
CacheUsage::AlwaysCache => ("uses", "caching always restored here"),
301339
CacheUsage::DefaultActionBehaviour => ("uses", "cache enabled by default here"),
302340
CacheUsage::DirectOptIn => ("with", "opt-in for caching here"),
303341
CacheUsage::ConditionalOptIn => ("with", "opt-in for caching might happen here"),

Diff for: tests/snapshot.rs

+6
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,12 @@ fn cache_poisoning() -> Result<()> {
328328
.workflow(workflow_under_test("cache-poisoning/issue-343-repro.yml"))
329329
.run()?);
330330

331+
insta::assert_snapshot!(zizmor()
332+
.workflow(workflow_under_test(
333+
"cache-poisoning/caching-not-configurable.yml"
334+
))
335+
.run()?);
336+
331337
Ok(())
332338
}
333339

Diff for: tests/snapshots/snapshot__cache_poisoning-12.snap

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
source: tests/snapshot.rs
3+
expression: "zizmor().workflow(workflow_under_test(\"cache-poisoning/caching-not-configurable.yml\")).run()?"
4+
snapshot_kind: text
5+
---
6+
error[cache-poisoning]: runtime artifacts potentially vulnerable to a cache poisoning attack
7+
--> @@INPUT@@:1:1
8+
|
9+
1 | / on:
10+
2 | | push:
11+
3 | | tags:
12+
4 | | - '**'
13+
| |____________^ generally used when publishing artifacts generated at runtime
14+
5 |
15+
...
16+
15 | - name: Setup CI caching
17+
16 | uses: Mozilla-Actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9
18+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ caching always restored here
19+
|
20+
= note: audit confidenceLow
21+
22+
1 finding: 0 unknown, 0 informational, 0 low, 0 medium, 1 high
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
on:
2+
push:
3+
tags:
4+
- '**'
5+
6+
jobs:
7+
publish:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- name: Project Checkout
11+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
12+
with:
13+
persist-credentials: false
14+
15+
- name: Setup CI caching
16+
uses: Mozilla-Actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9
17+
18+
- name: Publish on crates.io
19+
run: cargo publish --token ${{ secrets.CRATESIO_PUBLISH_TOKEN }}

0 commit comments

Comments
 (0)