A static analysis tool for finding common Forge vulnerabilities.
Usage: fsrt [OPTIONS] [DIRS]...
fsrt [OPTIONS] <COMMAND>
Arguments:
[DIRS]... The directory to scan. Assumes there is a `manifest.yaml` file in the top level directory, and that the source code is located in `src/`
Commands:
remote Interact with deployed Forge apps
Options:
-d, --debug
--dump-ir <DUMP_IR> Dump the IR for the specified function
-dt, --dump-dt <DUMP_DOM_TREE> Dump the Dominator Tree for the specified app
-f, --function <FUNCTION> A specific function to scan, must be an entrypoint specified in `manifest.yml`
-h, --help Print help information
-V, --version Print version information
--verbose Print diagnostics to stderr
--check-permissions Runs the permission checker
--cached-permissions Uses cached swagger permissions to avoid redownloading them
--cached-permissions-path <LOCATION> Uses the designated cache location, otherwise selects ~/.cache dir
--graphql-schema-path <LOCATION> Uses the graphql schema in location; othwerwise selects ~/.config dir
Run fsrt --help, fsrt remote --help, or fsrt remote <COMMAND> --help for current options.
The secret-logging scanner reports values returned by getSecret on the kvs
export (named or default) of @forge/kvs, or on the legacy storage export of
@forge/api, when they reach console.log, console.info, console.warn,
console.error or console.debug. It is disabled by default; select it
explicitly, alone or with other scanners:
fsrt --scanners secret-logging ./my-forge-app
fsrt --scanners secret,secret-logging ./my-forge-appimport { kvs } from '@forge/kvs';
export async function run() {
const token = await kvs.getSecret('token');
console.log(`Token: ${token}`); // reported
}It follows import aliases, assignments, expressions, arrays and objects,
branches, loops, helper arguments and returns, and single-handler .then()
fulfillment callbacks. It checks manifest entrypoints, resolver handlers, their
reachable helpers, and module initializers. Ordinary kvs.get calls and other
console methods (such as console.table) are outside this rule. Captured
bindings use their values at the helper call, and operators returning only
metadata (such as typeof, void, boolean negation, and comparisons) clear
secret taint.
A property written through a known path keeps its own taint, so reading
password from { host: secret.host, password: 'literal' } is clean, while
reading host from { host: secret } is reported. Any other property read from
a secret stays tracked, but is reported only when the last property name ends
with a secret suffix: secret[account].password and
const { apiToken } = secret.config are reported, while logging
secret[account], secret.host or secret.length is not. Names compare
case-insensitively after removing _ and -, and the longest matching suffix
decides, so the excluded pagetoken wins over token in nextPageToken.
--secret-logging-suffixes and --secret-logging-excluded-suffixes replace the
defaults listed by fsrt --help:
fsrt --scanners secret-logging \
--secret-logging-suffixes password,secret,apikey ./my-forge-appLogging a whole secret, an object or array containing one, or a method result
such as secret.trim() is reported. Splitting on or replacing the secret itself,
such as text.split(secret).join('[REDACTED]') or url.replace(secret, '***'),
redacts it. For now, so does partial masking: a slice, substring or substr
of at most 12 characters, bounded by literals or the string's own length (such
as key.slice(-4), key.substring(0, 12) or key.substring(key.length - 4)),
is treated as clean and logged as a warning (visible with FORGE_LOG=warn or
--verbose). Longer or unbounded substrings such as key.slice(7), other
truncation, and encoding are still reported.
lodash's omit, pick and get select properties when their paths are string
or array literals. They are recognized when imported from lodash or lodash-es
(by name, default or namespace import) or from a per-method package such as
lodash/omit or lodash.omit. omit({ url, token: secret }, ['token']) and
get(config, 'url') are clean, while pick(config, ['token']) is reported. A
secret read whole has no tracked properties, so omit(secret, ['password']) is
still reported. lodash/fp, require('lodash') and other libraries' omit
propagate like any other unresolved call.
The shared engine in crates/forge_analyzer/src/taint.rs propagates taint for a
scanner's TaintPolicy: its sources, property reads (property_taint, and
tracks_fields for written properties), sanitizing methods (method_taint) and
sink predicate (is_violation). A runner selects TaintDataflow<MyPolicy>,
passes its configured policy from Runner::dataflow, and can reuse
visit_taint_call for interprocedural traversal. Both this scanner and the
existing prototype-pollution checker use that engine; the defaults propagate
taint conservatively. The predicate sees values before each instruction; only
matching sink locations are retained for the diagnostic walk after analysis
converges. Variable vectors are retained at block boundaries, rather than copied
and stored at every instruction.
This is a conservative analysis: helper summaries combine call sites within an
entrypoint, and calls to unresolved functions (including HTTP clients other than
Forge fetch and the product request APIs) propagate argument/receiver taint, so
a response to an authenticated request can be reported. Dynamic dispatch,
mutation through object aliases, and callback APIs other than the modeled
fulfillment form can still miss flows; sanitization through unresolved functions
can produce warnings.
Callee summaries do not propagate writes to outer bindings back to callers.
Findings identify the sink function, unique IR body, and instruction location,
not a source line or a runtime secret value.
You will need to install Rust to compile FSRT. You can install Rust through Rustup or through your distro's package manager. You will also
need Cargo, which comes by default with most Rust toolchains.1
latest stable release, and adding the toolchain
Installing from source:
git clone https://github.com/atlassian-labs/FSRT.git
cd FSRT
cargo install --path crates/fsrt --lockedor alternatively:
cargo install --git https://github.com/atlassian-labs/FSRT --locked
remote interacts with deployed Forge apps: it creates Forge Context Tokens (FCTs) and
Forge Invocation Tokens (FITs), invokes extensions, and optionally saves a session cookie
through browser login. It uses the configured Atlassian account and app installation.
fsrt remote mint-fct <MODULE_KEY> [OPTIONS]
fsrt remote mint-fit <REMOTE_KEY> (--module <MODULE_KEY> | --fct <FCT>) [OPTIONS]
fsrt remote invoke-extension [FUNCTION] <MODULE_KEY> [--entrypoint <ENTRYPOINT>] [--fct <FCT>] [OPTIONS]
fsrt remote mint-cookie [--config <PATH>] [--headed]
The FCT, FIT, and extension-invocation commands resolve the app ID with the following precedence: the
--app-id <APP_ID> CLI argument, then app_id in the config file, then the app.id read from an app
manifest (via --app-dir <DIR>, defaulting to the current directory). Supplying the ID by CLI or config
skips the manifest entirely, so a deployed app can be accessed without a local checkout. The ID may be a
bare {uuid} or a full ari:cloud:ecosystem::app/{uuid} ARI. These commands also use a TOML
configuration file; see fsrt-remote.toml.example.
mint-fct mints an FCT for a deployed module. Pass --ctx '<JSON>' to populate the
selected extension's context object.
mint-fit supports two mutually exclusive ways to provide its FCT:
| Mode | Required inputs | Behavior |
|---|---|---|
| Use an existing FCT | <REMOTE_KEY> --fct <FCT> |
Uses the supplied FCT when --module and --ctx are omitted. |
| Mint a new FCT | <REMOTE_KEY> --module <MODULE_KEY> |
Mints an FCT with an empty context, then uses it to mint the FIT. Add --ctx '<JSON>' to set its context. |
invoke-extension calls a deployed Forge module directly, which is useful for testing its behavior
with custom payloads without going through the product UI. Pass a module key and, when needed, a
resolver function or --entrypoint; omitting --entrypoint leaves selection to the backend.
The command mints an FCT automatically unless one is supplied with --fct. Use --ctx to provide
invocation context.
By default, live mint commands print only the token, while invoke-extension prints the
backend response. --dry-run queries metadata without signing tokens or invoking the
extension and prints redacted request variables. --verbose prints live diagnostics to
stderr. Run fsrt remote <COMMAND> --help for all options.
mint-cookie opens Chrome, signs in to the configured Atlassian site, and stores the
resulting tenant.session.token in auth.raw_cookie_file. It is intentionally opt-in:
export ATL_PASSWORD='your-account-password'
cargo run -p fsrt --features mint_cookie -- remote mint-cookie --headedSet [cookie].username in fsrt-remote.toml; --headed opens a browser window for MFA,
bot checks, or other manual steps. Chrome must be installed, and the WebDriver may download
a matching driver. The generated cookie file is a bearer credential: keep it untracked and
do not paste it into logs, tickets, or chat.
To run the test suite:
cargo testThere are also two sample vulnerable Forge apps for testing. In the future these will be added to the test-suite, but
until then you can test fsrt by manually invoking:
fsrt ./test-apps/jira-damn-vulnerable-forge-appTesting with a GraphQl Schema:
cargo test --features graphql_schemaContributions to FSRT are welcome! Please see CONTRIBUTING.md for details.
Copyright (c) 2022 Atlassian and others.
FSRT is dual licensed under the MIT and Apache 2.0 licenses.
See LICENSE-APACHE and LICENSE-MIT for details.
Footnotes
-
Cargo is technically not required if you want to download every dependency, invoke
rustc, and link everything manually. However, I wouldn't recommend doing this unless you're extremely bored. ↩
