Forge context security: which parts can we trust, and why is it so hard to get right?
Forge apps receive context in many places: resolver req.context, getAppContext(), trigger payloads, and the Forge Invocation Token (FIT) on Forge Remote calls. For most of these, the docs don’t say which fields can be trusted. Where they do, another page often contradicts it, or what actually arrives contradicts it.
Docs that contradict each other or are missing
Resolver context
- Forge resolver (updated Nov 2024): “Not all of the values in the
contextparameter are guaranteed to be secure […] See App context security.” - App context security (updated Nov 2025): “Context parameters in each resolver function are guaranteed to be secure, unalterable, and valid to be used for authorization.”
- The previous version of that page said: “Only
license,accountId,accountType, andinstallContext[…] are guaranteed”. Was this an intentional change? Does it coverextension, for exampleextension.issue.idorextension.project.id? - App context security covers only resolvers and
getContext. It says nothing aboutgetAppContext(), trigger payloads, or the FIT.
app.module.typeis “the module type […] for front-end invocations. Otherwise, it will becore:endpoint”. In practice,invokeRemotefrom a web-trigger function sends""for bothapp.module.typeand the requiredapp.module.key(my report, acknowledged as a known gap for all hosted compute → remote calls in FRGE-1933). Endpoint-backed triggers send their own module type (core:trigger,core:scheduledTrigger,core:preUninstall). When iscore:endpointactually sent?principalis documented as “UI modules only”. It is present on backendinvokeRemotecalls whenever a user is behind them (FRGE-2031, confirmed by staff).- Until 25 Sep 2026, backend
contextwas documented as empty, although it containedcloudId. The docs were corrected after a partner reported it (FRGE-2277). - The
requestRemotepage lists no FIT claims. It says the remote “must also verify the contextual claims”, but not which claims exist or can be trusted. Some context is missing entirely: ECO-1678 reports no project context inrequestRemoteFITs forjira:workflowPostFunction. - RFC-138 says each module’s owning team “must analyse and determine which fields have adequate permission checks and should legitimately remain in the signed, trusted context”. Does that mean some fields in today’s signed FIT context lack permission checks? If so, which ones? As highlighted in the RFC, I think this is a step in the right direction, but if it isn’t done holistically done across the platform, it will only fix one small part.
Telling frontend and backend calls apart in a remote
A remote can be called in three ways:
@forge/apiinvokeRemotefrom a Forge function, for example, a resolver that has already checked permissions.@forge/bridgeinvokeRemotefrom the browser. It accepts anypathand custom headers, sent to the remote behind the module’sresolver.endpoint.@forge/bridgerequestRemotefrom the browser. It can target any remote in the manifest. Requests “are not proxied through the Forge platform”, so the browser sends the FIT itself.
An endpoint meant only for (1) can be called directly from the browser by any user who can render a UI module. For requestRemote, that includes JSM customer portal modules. I believe there is no documented FIT claim that tells (1) apart from (2) or (3).
In FRGE-1927, Atlassian suggests a workaround for getting trusted context to a remote: “invoke your remote from within the Forge function and pass along the appropriate details with whatever security controls you deem fit”. That only works if the remote can verify that the call came from the Forge function, otherwise, it brings us back to the same gap.
If Atlassian needs more evidence, I am fairly certain a review of submissions in Marketplace partners’ bug bounty programs would show that many come down to uncertainty or assumptions about these context contracts.
Proposal: one context contract
I think it would be great to have one reference page that covers every place an app receives context, and keep it current. Ideally, this will not be hand-crafted but generated from the source of truth:
- Every surface: resolver
req.context,view.getContext()and UI Kit hooks,getAppContext(), trigger and consumer arguments, and the FIT for each caller:@forge/apiinvokeRemote(per function type), bridgeinvokeRemote,requestRemote, endpoint-backed triggers, andinvokeService. - Shape: for each surface, the fields, whether they are required or optional, and how they vary by module (for example,
extension). - Trust: for each field, whether the platform sets and permission-checks it, so it is safe for authorization, or whether it can be influenced from the browser.
- Caller: a documented, signed way to tell which surface produced an invocation, at minimum a Forge function versus the browser.
- Changes: versioned, with changes announced in the changelog, so we don’t discover them from runtime behavior.
- Coordination: better Forge platform coordination between teams that develop individual modules/context seams to have a more consistent experience
Open questions/requests
- Which resolver
req.contextfields are trusted, includingextension. RFC-138 takes a stab at this forrequestRemote, but it only covers one small part of all the seams. - How can we tell if a call came from a Forge function or from the browser (
invokeRemoteorrequestRemote)? Or is there a documented claim in the FIT we can rely on? - Document the
requestRemoteFIT claim set.
Forge is evolving quickly, which is great, but it also means docs, concepts, and contracts must keep pace. We build on Forge every day, and the seams between frontend, backend, and remote have turned into guesswork and trial and error. They are getting harder to get right, not easier.