Getting it into your agent
One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.
npx agentmods add rules/atlassian/forge-skills/missing-resolver-authzgit clone --depth 1 https://github.com/atlassian/forge-skillsWhat it costs to keep this loaded
Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.
| Model | Per session | Once invoked |
|---|---|---|
| Fable 5 | $0.00011 | $0.02702 |
| Opus 5 | $0.00005 | $0.01351 |
| Sonnet 5 | $0.00002 | $0.00540 |
| Haiku 4.5 | $0.00001 | $0.00270 |
Grade A, and why
missing-resolver-authz scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured yesterday.
A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.
Nothing flagged
None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.
How it starts
The opening of the file, as written. The whole thing — 280 lines — stays where its author put it; the contents beside it link to each section on GitHub.
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:
payloadparameter in resolver definitions,contextobject. - 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
// 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
// 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 };
});
What this file has done since we first saw it
Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.
- yesterday First seen · 280 lines · 11 tokens per session scan A c2e42452787e
missing-resolver-authz is a cursor rule published in the GitHub repository atlassian/forge-skills (20 stars, last pushed 2d ago), licensed Apache-2.0. It adds 11 tokens to every session and 2,702 once invoked, about $0.0001 per session on Opus 5. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-30.
Other cursor rules, from other repositories
project
4DA project rules and conventions.
angular-20
This rule provides comprehensive best practices and coding standards for Angular development, focusing on modern TypeScript, standalone components, signals, and performance optimizations.
dev-standard
Apache Superset development standards and guidelines for Cursor IDE.
typescript
Changes to these high-fan-out internals can affect every message, delta, element, or rerun. Keep work in them minimal, and benchmark changes with representative stress-test apps.
coolify-ai-docs
Master reference to all Coolify AI documentation in .ai/ directory.
python_lib
Tips and guidelines specific to the development of the Streamlit Python library, not applicable to scripts and e2e tests.