Skip to content

Commit 1ae6c36

Browse files
author
gersbach
committed
EAS-2582 : Map Scopes to OAuth
1 parent 7fa85d2 commit 1ae6c36

File tree

5 files changed

+98
-43
lines changed

5 files changed

+98
-43
lines changed

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Arguments:
2121
-f, --function <FUNCTION> A specific function to scan. Must be an entrypoint specified in `manifest.yml`
2222
-h, --help Print help information
2323
-V, --version Print version information
24+
--check-permissions Runs the permission checker
25+
--graphql-schema-path <LOCATION> Uses the graphql schema in location; othwerwise selects ~/.config dir
2426
```
2527

2628
## Installation
@@ -64,6 +66,12 @@ until then you can test `fsrt` by manually invoking:
6466
fsrt ./test-apps/jira-damn-vulnerable-forge-app
6567
```
6668

69+
Testing with a GraphQl Schema:
70+
71+
```sh
72+
cargo test --features graphql_schema
73+
```
74+
6775
## Contributions
6876

6977
Contributions to FSRT are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.

crates/forge_analyzer/src/checkers.rs

+17-16
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use itertools::Itertools;
55
use smallvec::SmallVec;
66
use std::{
77
cmp::max,
8+
collections::HashSet,
89
iter::{self, zip},
910
mem,
1011
ops::ControlFlow,
@@ -1009,7 +1010,7 @@ impl PermissionDataflow {
10091010
}
10101011
}
10111012

1012-
impl WithCallStack for PermissionVuln {
1013+
impl<'a> WithCallStack for PermissionVuln<'a> {
10131014
fn add_call_stack(&mut self, _stack: Vec<DefId>) {}
10141015
}
10151016

@@ -1206,12 +1207,12 @@ impl<'cx> Dataflow<'cx> for PermissionDataflow {
12061207
}
12071208
}
12081209

1209-
pub struct PermissionChecker {
1210+
pub struct PermissionChecker<'a> {
12101211
pub visit: bool,
1211-
pub vulns: Vec<PermissionVuln>,
1212+
pub vulns: Vec<PermissionVuln<'a>>,
12121213
}
12131214

1214-
impl PermissionChecker {
1215+
impl<'a> PermissionChecker<'a> {
12151216
pub fn new() -> Self {
12161217
Self {
12171218
visit: false,
@@ -1221,22 +1222,22 @@ impl PermissionChecker {
12211222

12221223
pub fn into_vulns(
12231224
mut self,
1224-
permissions: Vec<String>,
1225-
) -> impl IntoIterator<Item = PermissionVuln> {
1225+
permissions: HashSet<&'a String>,
1226+
) -> impl IntoIterator<Item = PermissionVuln<'a>> {
12261227
if !permissions.is_empty() {
12271228
self.vulns.resize(1, PermissionVuln::new(permissions));
12281229
}
12291230
self.vulns
12301231
}
12311232
}
12321233

1233-
impl Default for PermissionChecker {
1234+
impl<'a> Default for PermissionChecker<'a> {
12341235
fn default() -> Self {
12351236
PermissionChecker::new()
12361237
}
12371238
}
12381239

1239-
impl fmt::Display for PermissionVuln {
1240+
impl<'a> fmt::Display for PermissionVuln<'a> {
12401241
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12411242
write!(f, "Permission vulnerability")
12421243
}
@@ -1265,12 +1266,12 @@ impl JoinSemiLattice for PermissionTest {
12651266
}
12661267

12671268
#[derive(Debug, Clone)]
1268-
pub struct PermissionVuln {
1269-
unused_permissions: Vec<String>,
1269+
pub struct PermissionVuln<'a> {
1270+
unused_permissions: HashSet<&'a String>,
12701271
}
12711272

1272-
impl PermissionVuln {
1273-
pub fn new(unused_permissions: Vec<String>) -> PermissionVuln {
1273+
impl<'a> PermissionVuln<'a> {
1274+
pub fn new(unused_permissions: HashSet<&'_ String>) -> PermissionVuln<'_> {
12741275
PermissionVuln { unused_permissions }
12751276
}
12761277
}
@@ -1279,7 +1280,7 @@ pub struct DefinitionAnalysisRunner {
12791280
pub needs_call: Vec<(DefId, Vec<Operand>, Vec<Value>)>,
12801281
}
12811282

1282-
impl<'cx> Runner<'cx> for PermissionChecker {
1283+
impl<'cx, 'a> Runner<'cx> for PermissionChecker<'a> {
12831284
type State = PermissionTest;
12841285
type Dataflow = PermissionDataflow;
12851286

@@ -1295,11 +1296,11 @@ impl<'cx> Runner<'cx> for PermissionChecker {
12951296
}
12961297
}
12971298

1298-
impl Checker<'_> for PermissionChecker {
1299-
type Vuln = PermissionVuln;
1299+
impl<'a> Checker<'_> for PermissionChecker<'a> {
1300+
type Vuln = PermissionVuln<'a>;
13001301
}
13011302

1302-
impl IntoVuln for PermissionVuln {
1303+
impl<'a> IntoVuln for PermissionVuln<'a> {
13031304
fn into_vuln(self, reporter: &Reporter) -> Vulnerability {
13041305
Vulnerability {
13051306
check_name: "Least-Privilege".to_owned(),

crates/fsrt/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ license.workspace = true
88
[lints]
99
workspace = true
1010

11+
[features]
12+
graphql_schema = []
13+
1114
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1215

1316
[dependencies]

crates/fsrt/src/main.rs

+59-13
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use std::{
2020

2121
use graphql_parser::{
2222
query::{Mutation, Query, SelectionSet},
23-
schema::ObjectType,
23+
schema::{EnumType, EnumValue, ObjectType},
2424
};
2525

2626
use graphql_parser::{
@@ -118,7 +118,7 @@ impl fmt::Display for Error {
118118
}
119119

120120
struct PermissionsAndNextSelection<'a, 'b> {
121-
permission_vec: Vec<String>,
121+
permission_vec: Vec<&'a str>,
122122
next_selection: NextSelection<'a, 'b>,
123123
}
124124

@@ -130,7 +130,7 @@ struct NextSelection<'a, 'b> {
130130
fn parse_grapqhql_schema<'a: 'b, 'b>(
131131
schema_doc: &'a [graphql_parser::schema::Definition<'a, &'a str>],
132132
query_doc: &'b [graphql_parser::query::Definition<'b, &'b str>],
133-
) -> Vec<String> {
133+
) -> Vec<&'a str> {
134134
let mut permission_list = vec![];
135135

136136
// dequeue of (parsed_query_selection: SelectionSet, schema_type_field: Field)
@@ -272,7 +272,7 @@ fn get_type_or_typex_with_name<'a, 'b>(
272272
.flatten()
273273
}
274274

275-
fn get_field_directives<'a>(field: &'a graphql_parser::schema::Field<'_, &'a str>) -> Vec<String> {
275+
fn get_field_directives<'a>(field: &'a graphql_parser::schema::Field<'_, &'a str>) -> Vec<&'a str> {
276276
let mut perm_vec = vec![];
277277
field.directives.iter().for_each(|directive| {
278278
if directive.name == "scopes" {
@@ -281,7 +281,7 @@ fn get_field_directives<'a>(field: &'a graphql_parser::schema::Field<'_, &'a str
281281
if let query::Value::List(val) = &arg.1 {
282282
val.iter().for_each(|val| {
283283
if let query::Value::Enum(en) = val {
284-
perm_vec.push(en.to_string());
284+
perm_vec.push(*en);
285285
}
286286
});
287287
}
@@ -295,7 +295,7 @@ fn get_field_directives<'a>(field: &'a graphql_parser::schema::Field<'_, &'a str
295295
fn check_graphql_and_perms<'a>(
296296
val: &'a Value,
297297
path: &'a graphql_parser::schema::Document<'a, &'a str>,
298-
) -> Vec<String> {
298+
) -> Vec<&'a str> {
299299
let mut operations = vec![];
300300

301301
match val {
@@ -488,7 +488,7 @@ pub(crate) fn scan_directory<'a>(
488488
);
489489
reporter.add_app(opts.appkey.clone().unwrap_or_default(), name.to_owned());
490490

491-
let mut perm_interp = Interp::<PermissionChecker>::new(
491+
let mut perm_interp = Interp::<PermissionChecker<'_>>::new(
492492
&proj.env,
493493
false,
494494
true,
@@ -618,6 +618,40 @@ pub(crate) fn scan_directory<'a>(
618618
&mut path.to_owned()
619619
};
620620

621+
let mut scope_path = path.clone();
622+
623+
scope_path.push("schema/shared/agg-shared-scopes.nadel");
624+
625+
let scope_map = fs::read_to_string(&scope_path).unwrap_or_default();
626+
627+
let ast = parse_schema::<&str>(&scope_map).unwrap_or_default();
628+
629+
let mut scope_name_to_oauth = HashMap::new();
630+
631+
ast.definitions.iter().for_each(|val| {
632+
if let graphql_parser::schema::Definition::TypeDefinition(TypeDefinition::Enum(
633+
EnumType { values, .. },
634+
)) = val
635+
{
636+
values.iter().for_each(
637+
|EnumValue {
638+
directives, name, ..
639+
}| {
640+
if let Some(directive) = directives.first() {
641+
if let graphql_parser::schema::Value::String(oauth_scope) = &directive
642+
.arguments
643+
.first()
644+
.expect("Should only be one directive")
645+
.1
646+
{
647+
scope_name_to_oauth.insert(*name, &**oauth_scope);
648+
}
649+
}
650+
},
651+
)
652+
}
653+
});
654+
621655
path.push("schema/*/*.nadel");
622656

623657
let joined_schema = glob(path.to_str().unwrap_or_default())
@@ -629,21 +663,21 @@ pub(crate) fn scan_directory<'a>(
629663
let ast = parse_schema::<&str>(&joined_schema);
630664

631665
if let std::result::Result::Ok(doc) = ast {
632-
let mut used_graphql_perms: Vec<String> = definition_analysis_interp
666+
let mut used_graphql_perms: Vec<&str> = definition_analysis_interp
633667
.value_manager
634668
.varid_to_value_with_proj
635669
.values()
636670
.flat_map(|val| check_graphql_and_perms(val, &doc))
637671
.collect();
638672

639-
let graphql_perms_varid: Vec<String> = definition_analysis_interp
673+
let graphql_perms_varid: Vec<&str> = definition_analysis_interp
640674
.value_manager
641675
.varid_to_value
642676
.values()
643677
.flat_map(|val| check_graphql_and_perms(val, &doc))
644678
.collect();
645679

646-
let graphql_perms_defid: Vec<String> = definition_analysis_interp
680+
let graphql_perms_defid: Vec<&str> = definition_analysis_interp
647681
.value_manager
648682
.defid_to_value
649683
.values()
@@ -653,14 +687,26 @@ pub(crate) fn scan_directory<'a>(
653687
used_graphql_perms.extend_from_slice(&graphql_perms_defid);
654688
used_graphql_perms.extend_from_slice(&graphql_perms_varid);
655689

656-
let final_perms: Vec<&String> = perm_interp
690+
let oauth_scopes: HashSet<&str> = used_graphql_perms
691+
.iter()
692+
.copied()
693+
.filter_map(|val| {
694+
if !scope_name_to_oauth.contains_key(&val) {
695+
warn!("Scope is not contained in the scope definitions")
696+
}
697+
698+
scope_name_to_oauth.get(&val).copied()
699+
})
700+
.collect();
701+
702+
let final_perms: HashSet<&String> = perm_interp
657703
.permissions
658704
.iter()
659-
.filter(|f| !used_graphql_perms.contains(&**f))
705+
.filter(|f| !oauth_scopes.contains(f.as_str()))
660706
.collect();
661707

662708
if run_permission_checker && !final_perms.is_empty() {
663-
reporter.add_vulnerabilities([PermissionVuln::new(perm_interp.permissions)]);
709+
reporter.add_vulnerabilities([PermissionVuln::new(final_perms)]);
664710
}
665711
}
666712

0 commit comments

Comments
 (0)