Hi Atlassian Forge community,
I have a security design question about Custom UI, Forge resolvers, and storing administrative configuration in Forge KVS.
In a Forge app, an administrator can update some configuration from the frontend. The frontend sends data using invoke(), the resolver receives { payload, context }, and then the backend stores the final configuration in KVS.
For example, imagine a generic configuration for a user history feature:
{
enabled: true,
displayMode: "compact",
maxItems: 20,
showUserName: true,
showDate: true,
allowedEvents: ["CREATED", "UPDATED", "DELETED"],
version: 3
}
The expected backend contract is:
enabledmust be boolean.displayModemust be one of:compact,detailed.maxItemsmust be a number within an accepted range.showUserNameandshowDatemust be boolean.allowedEventsmust only contain known internal event names.- Unknown fields should be rejected or ignored.
- The storage key should be defined in the backend, not received from the frontend.
updatedByshould come from the resolvercontext.accountId, not from the payload.
During a security review, we tested the scenario where a browser request generated by the frontend is captured and replayed or modified while the administrator session is still valid.
For example, one scenario we tested is copying a browser request generated by the frontend and replaying it from an external tool such as Postman or another HTTP client.
Suppose the copied request belongs to an authenticated administrator session and it calls a resolver function that creates or updates a record stored in KVS. If the request still contains valid authentication/session data and the resolver checks that the current user has the required permissions, the backend may treat the request as coming from a valid administrator.
In that situation, the request may still be able to create or update data in KVS, even if it was not triggered through the normal UI flow.
There are three different cases we are trying to understand.
First, the payload could be modified with invalid or unexpected data:
{
enabled: "true",
displayMode: "admin",
maxItems: 999999,
showUserName: true,
storageKey: "another-key",
updatedBy: "fake-user",
context: {
accountId: "fake-account-id",
moduleKey: "fake-module-key"
},
isAdmin: true,
extraConfig: {
unexpected: true
}
}
In this case, the backend can reject the request using strict schema validation, allowlisted fields, type validation, and backend-generated metadata.
However, the second case is more complex: the modified value may still be structurally valid.
For example, suppose the current stored configuration is:
{
enabled: true,
version: 3
}
The frontend sends a valid request to update the value:
{
enabled: false
}
In this case, false is still a valid boolean value. A strict schema validation would accept it because the type and structure are correct.
The third case is replaying the same or modified valid request outside the normal UI flow, for example from Postman, while the administrator session or authentication data is still valid.
My question is: if someone captures a valid request from an administrator session and replays or modifies it with another valid value, who is responsible for preventing or mitigating that scenario?
My current understanding is that the platform can authenticate the session and provide the real resolver context, but the app developer is responsible for deciding whether a valid administrative change should be accepted according to the app’s own business rules.
Because of that, we are considering the responsibility in layers:
1. Atlassian / Forge platform:
- Authenticate the request.
- Provide the real resolver context.
- Enforce platform-level session and token handling.
2. The user / administrator:
- Protect their authenticated session and browser environment.
- Avoid exposing session data or copied requests.
3. The app developer:
- Treat the payload as untrusted.
- Re-check permissions in the backend.
- Validate the payload with a strict schema.
- Validate business rules before writing to KVS.
- Avoid trusting context-like values sent from the frontend.
- Add audit logs.
- Consider replay mitigation for sensitive writes.
The part we are unsure about is whether Forge provides any built-in anti-replay protection for resolver invocations copied from the browser and executed outside the UI while the authenticated session is still valid.
If the copied request contains valid authentication/session data, is the app expected to handle this scenario at the application level?
For example, should the app implement additional backend protections such as:
1. Re-checking permissions on every request.
2. Using only the real resolver context for authorization.
3. Ignoring any context-like or permission-related values sent inside the payload.
4. Validating the payload with a strict backend schema.
5. Rejecting unknown fields.
6. Building a safe object in the backend instead of storing the payload directly.
7. Comparing the requested change against the current stored value.
8. Using a version field or optimistic locking.
9. Sending an expected previous value and rejecting the update if the stored value changed.
10. Creating an audit log with oldValue, newValue, updatedBy, and updatedAt.
11. Optionally using short-lived operation tokens or idempotency keys for sensitive writes.
12. Rejecting stale or repeated update attempts.
For example, instead of only sending:
{
enabled: false
}
The frontend could send:
{
enabled: false,
expectedPreviousValue: true,
version: 3
}
Then the resolver would read the current value from KVS and only apply the update if the stored version and expected previous value still match.
If they do not match, the resolver rejects the update because the request may be stale, replayed, or based on an outdated state.
We also noticed that some contextual information could be passed from the frontend as part of the payload. Our understanding is that this should not be used for authorization. Any context, accountId, moduleKey, isAdmin, storageKey, or permission-related value sent inside the payload should be ignored by the backend.
Instead, the resolver should only use the real Forge resolver context received by the backend function.
For example, the frontend should only send business data:
await invoke("saveConfiguration", {
data: {
enabled: true,
displayMode: "compact",
maxItems: 20,
version: 3
}
});
And the resolver should use the backend resolver context:
resolver.define("saveConfiguration", async ({ payload, context }) => {
// Use the resolver context for authorization.
// Do not use payload.context or any permission-related value from the frontend.
});
The backend flow we are considering is:
1. Receive { payload, context } in the resolver.
2. Validate the real Forge resolver context.
3. Check the current user’s permissions in the backend.
4. Ignore any context-like or permission-related values sent inside the payload.
5. Validate the payload against a strict backend schema.
6. Reject unknown fields or build a new safe object using only allowed fields.
7. Normalize values.
8. Read the current stored value from KVS if the operation modifies existing state.
9. Compare version and/or expected previous value.
10. Add metadata from the backend context, such as updatedBy and updatedAt.
11. Store only the safe object in KVS.
12. Create an audit log.
Example of the final object stored in KVS:
{
enabled: false,
displayMode: "compact",
maxItems: 20,
showUserName: true,
showDate: true,
allowedEvents: ["CREATED", "UPDATED"],
updatedBy: context.accountId,
updatedAt: "2026-06-18T00:00:00.000Z",
version: 4
}
We also considered encrypting the payload in the frontend and decrypting it in the resolver. However, we are not sure whether this provides real security if the encryption key is also available in the frontend. If the key is shipped to the browser, then someone capturing or inspecting the frontend code may also be able to obtain the key and generate a valid encrypted payload.
Because of that, our current assumption is that frontend encryption may only provide obfuscation, but it should not replace backend authorization, strict schema validation, normalization, safe object construction, state/version checks, and audit logs.
My questions are:
-
Is this the recommended security approach for Forge apps that store administrative configuration in KVS?
-
Should every payload sent from Custom UI to a resolver be considered untrusted, even when the resolver context belongs to a valid administrator?
-
If a previous implementation passed
contextfrom the frontend inside the payload, is the recommended fix to stop sending it and rely only on the resolvercontextfor authorization? -
Should the backend ignore any
context,accountId,moduleKey,isAdmin,storageKey, or permission-related values sent inside the payload? -
For simple fields such as booleans, should the backend accept only real boolean values (
true/false) and reject values like"true",1,0,null, arrays, or objects? -
If a captured request is modified with another structurally valid value, for example changing a boolean from
truetofalse, is the app developer responsible for detecting or mitigating that through business rules, version checks, expected previous values, audit logs, or similar controls? -
If a copied request is replayed from a tool such as Postman while the administrator session or authentication data is still valid, and the resolver receives a valid
contextand a structurally validpayload, where does the responsibility fall? -
Does Forge provide any built-in protection against replaying a copied
invoke()request outside the normal UI flow while the administrator session is still valid? -
For internal configuration values that cannot be validated against Jira REST APIs, is it enough to validate against the app’s own strict backend schema and reject unknown fields?
-
Does encrypting the payload in the frontend add any meaningful security if the encryption key is also available in the frontend?
-
Should the app handle replay mitigation with backend validation, audit logs, version checks, expected previous values, idempotency keys, operation tokens, or optimistic locking?
-
In the shared responsibility model, where does the responsibility fall for preventing invalid, unnecessary, replayed, or modified administrative payloads from being stored in KVS: Atlassian platform security, the app developer, the administrator/user, or all of them in different layers?
Any guidance or recommended pattern would be appreciated.