NAC3 -- security model
Spec version: 2.2 stable (+ v2.3 interop preview).
Threat model
NAC3 sits between agents and your UI. It is a contract layer, not an authentication layer. Several distinct trust boundaries exist; this doc names them so you can think clearly about which ones NAC3 protects and which ones it does not.
Boundary A: User -> UI
Out of scope for NAC. Use your existing auth (sessions, OAuth, SSO, MFA). Once a user is authenticated, NAC3 assumes any action the user could take in the UI is allowed.
Boundary B: User-controlled agent -> UI
A user grants an agent permission to drive their browser session. Examples: a voice assistant, a screen reader, an LLM chat client embedded in the same page. NAC3's job here:
without scraping coordinates.
clicks for security-sensitive verbs (payment, deletion, role grants). The agent cannot forge isTrusted=true; only a real user gesture sets it.
without re-reading the DOM.
- Provide the agent with stable element names so it can act
- Surface
event.isTrustedso the host can refuse synthetic - Provide event-level acks so the agent knows what completed
NAC3 does NOT protect against an agent that the user explicitly trusted misusing that trust. That is a user-experience problem (consent prompts before sensitive verbs) handled by your app, not by NAC.
Boundary C: External service -> UI (LLM intermediary)
If a user's voice prompt is shipped to a remote LLM that returns NAC3 actions, the LLM becomes a trust principal. NAC3's job here:
snapshot + the registered manifests). It does not see the user's auth tokens, cookies, or DOM internals beyond what the manifest declares.
action; the chat client validates it (does the nac_id exist? is the verb allowed?) before dispatching.
in the snapshot it sent (prevents prompt injection that smuggles in arbitrary ids).
- The LLM only sees what
NAC.describe()exposes (the tree - The LLM cannot directly cause a click. It returns a structured
- The chat client SHOULD reject actions whose
nac_idwas not
NAC3 does NOT prescribe the LLM's prompt template, rate limits, or filtering. See guides/LLM_WIRING.md for recommendations.
Boundary D: Tenant -> Tenant (multi-tenant deployments)
Multi-tenant SaaS where tenants share a runtime but not data. NAC3 protects this with HMAC-signed manifests:
over a stable serialisation, using a per-tenant secret stored server-side.
secret it expects for the active tenant. If the signature mismatches, the manifest is rejected.
without the signing secret.
- Each tenant ships its manifest with an HMAC signature computed
- The runtime, on
NAC.register(), recomputes the HMAC using the - A malicious tenant cannot forge another tenant's manifest
NAC3 does NOT prevent a tenant from registering an excessively large or malformed manifest beyond a basic size cap; rate-limit manifest registration server-side if you accept untrusted ones.
Boundary E: Malicious script -> Page
A page that includes attacker-controlled JS (XSS, supply chain compromise) is already lost. NAC3 cannot help here; the attacker can call NAC.click(...) directly. Mitigate via CSP, SRI, and your usual web security stack.
Provenance signals
is_trusted in success events
Every action success event detail carries is_trusted: boolean. A host can require this for sensitive verbs:
document.addEventListener('nac:action:succeeded', function (e) {
var d = e.detail;
if (d.action_id === 'invoice.delete' && !d.is_trusted) {
console.warn('[security] refused synthetic delete click');
e.preventDefault();
}
});
The reference demo example-v20-full.php includes a button pair (v20_panel.istrusted_real and v20_panel.istrusted_fake) that demonstrates the distinction in the panel output.
HMAC manifest signing
Server-side, generate the signature:
import hmac, hashlib, json
manifest_body = json.dumps(manifest, sort_keys=True, separators=(',', ':'))
sig = hmac.new(
tenant_secret.encode('utf-8'),
manifest_body.encode('utf-8'),
hashlib.sha256
).hexdigest()
manifest['provenance'] = {
'signed_at': now_iso8601(),
'signed_by': tenant_slug,
'signature': sig
}
Client-side:
NAC.set_provenance_secret(SECRET_FROM_AUTHED_RESPONSE);
NAC.register(manifest);
// runtime recomputes hmac, rejects if mismatch
The secret MUST come from an authenticated server response; never embed it in JS source. Rotate per-session if the threat model demands it.
Reporting a vulnerability
Email nac@yujin.dev with:
- Description of the vulnerability.
- Reproduction steps or proof-of-concept.
- Affected NAC3 version(s).
- Suggested mitigation if you have one.
Do NOT open a public GitHub issue. We commit to:
- Acknowledge receipt within 3 business days.
- Provide a triage assessment within 10 business days.
- Coordinate disclosure timing with the reporter.
Critical issues affecting the public spec ship with a patch release within 30 days; lower severity within 90.
What NAC3 explicitly does NOT do
page; if you want isolation, run separate pages).
tracked as v3.0 candidate.
- Authenticate users.
- Encrypt data in transit (use TLS).
- Prevent the user from doing what the user is allowed to do.
- Sandbox agents away from each other (they all run in the same
- Sign individual actions (only manifests). Per-action signing is