Skip to content

Commit 934192c

Browse files
authored
EAS-4405 - feat: whitelist endpoint for authz scanner (#85)
* feat: whitelist endpoint for authz scanner * chore: fmt fix * validate allowlist --------- Co-authored-by: gladstone-9 <gabejgladstone@gmail.com>
1 parent 6af1000 commit 934192c

2 files changed

Lines changed: 89 additions & 1 deletion

File tree

crates/forge_analyzer/src/definitions.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,9 @@ enum ApiCallKind {
933933
Authorize,
934934
}
935935

936+
/// Specific API path prefixes that are unconditionally safe (trivial) for AuthZ purposes.
937+
const TRIVIAL_API_PREFIXES: &[&str] = &["/wiki/api/v2/app/properties"];
938+
936939
fn classify_api_call(expr: &Expr) -> ApiCallKind {
937940
use regex::Regex;
938941
use std::cell::OnceCell;
@@ -959,7 +962,10 @@ fn classify_api_call(expr: &Expr) -> ApiCallKind {
959962
fn check(&mut self, name: &str) {
960963
if name.contains("permission") {
961964
self.kind = self.kind.max(ApiCallKind::Authorize);
962-
} else if trivial_regex(name) || name.contains("group") {
965+
} else if trivial_regex(name)
966+
|| name.contains("group")
967+
|| TRIVIAL_API_PREFIXES.iter().any(|p| name.contains(p))
968+
{
963969
self.kind = self.kind.max(ApiCallKind::Trivial);
964970
} else if name == "/rest/api/3/field" || name == "/rest/api/2/field" {
965971
self.kind = ApiCallKind::Fields;

crates/fsrt/src/test.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,88 @@ fn authz_function_called_in_object() {
578578
assert!(scan_result.contains_vulns(1))
579579
}
580580

581+
// Tests that calls to /wiki/api/v2/app/properties are in TRIVIAL_API_PREFIXES and
582+
// are not flagged by the AuthZ scanner even without a prior authorize() call.
583+
#[test]
584+
fn wiki_api_v2_app_properties_not_flagged_by_authz() {
585+
let test_forge_project = MockForgeProject::files_from_string(
586+
"// src/index.tsx
587+
import ForgeUI, { render, Fragment, Macro, Text } from '@forge/ui';
588+
import api, { route } from '@forge/api';
589+
590+
const App = async () => {
591+
const res = await api.asApp().requestConfluence(route`/wiki/api/v2/app/properties`);
592+
return (
593+
<Fragment>
594+
<Text>Hello world!</Text>
595+
</Fragment>
596+
);
597+
};
598+
599+
export const run = render(<Macro app={<App />} />);
600+
601+
// manifest.yml
602+
modules:
603+
macro:
604+
- key: basic-hello-world
605+
function: main
606+
title: basic
607+
handler: nothing
608+
description: Inserts Hello world!
609+
function:
610+
- key: main
611+
handler: index.run
612+
app:
613+
id: ari:cloud:ecosystem::app/07b89c0f-949a-4905-9de9-6c9521035986
614+
permissions:
615+
scopes: []",
616+
);
617+
618+
let scan_result = scan_directory_test(test_forge_project);
619+
assert!(scan_result.contains_vulns(0))
620+
}
621+
622+
// A non-allowlisted Confluence path without authorize() SHOULD be flagged,
623+
// confirming wiki_api_v2_app_properties_not_flagged_by_authzis is actually exercising the allowlist.
624+
#[test]
625+
fn non_allowlisted_confluence_path_flagged_by_authz() {
626+
let test_forge_project = MockForgeProject::files_from_string(
627+
"// src/index.tsx
628+
import ForgeUI, { render, Fragment, Macro, Text } from '@forge/ui';
629+
import api, { route } from '@forge/api';
630+
631+
const App = async () => {
632+
const res = await api.asApp().requestConfluence(route`/wiki/api/v2/pages`);
633+
return (
634+
<Fragment>
635+
<Text>Hello world!</Text>
636+
</Fragment>
637+
);
638+
};
639+
640+
export const run = render(<Macro app={<App />} />);
641+
642+
// manifest.yml
643+
modules:
644+
macro:
645+
- key: basic-hello-world
646+
function: main
647+
title: basic
648+
handler: nothing
649+
description: Inserts Hello world!
650+
function:
651+
- key: main
652+
handler: index.run
653+
app:
654+
id: ari:cloud:ecosystem::app/07b89c0f-949a-4905-9de9-6c9521035986
655+
permissions:
656+
scopes: []",
657+
);
658+
659+
let scan_result = scan_directory_test(test_forge_project);
660+
assert!(scan_result.contains_vulns(1))
661+
}
662+
581663
#[test]
582664
fn secret_vuln_fetch_header() {
583665
let test_forge_project = MockForgeProject::files_from_string(

0 commit comments

Comments
 (0)