Specification · v1 Working Draft
anvil·md

Specification  ·  Section 8 of 14

Security

Stamps are untrusted input, stamping is permission-gated, and attribute-position values are allowlisted rather than escaped.

8.1 A stamp is untrusted input

Stamp values are attacker-influenced free text: anyone with interact permission typed them. Handing that to a model as an ordinary user turn is worse than it first looks, because ANVIL text sits right next to the agent’s own markdown and reads like it.

Wrap it. The delivered turn must:

  • carry an explicit untrusted marker (a channel, a wrapper element, a system preamble — whatever your transport has),
  • fence the values as quoted data rather than prose,
  • state plainly that the contents are data, not instructions.

Keep the framing function pure and side-effect free, so the delivery path and its tests share exactly one implementation.

8.2 Stamping is permission-gated

A read-only viewer must not be able to stamp a block. Enforcement belongs on the server, on the stamp write; hiding the controls client-side is cosmetic.

Give it its own permission. Do not fold it into a general “can read the conversation” grant — being able to see a question is not consent to answer it on someone else’s behalf.

8.3 Agent-authored values in attribute position

swatch, font and img land in style and src attributes, where escaping is not sufficient. They are allowlisted:

ValueRule
swatchhex only, /^#[0-9a-f]{3,8}$/i
fontconservative family name; no quotes, no url(, no escapes
imghttp(s) only

A value that fails is dropped, not escaped. This is implemented and tested in @anvil-md/render-html.

Everything else — prompts, labels, hints, placeholders — is HTML-escaped.

8.3.1 A brand mark is not one of these, and cannot be.

The three rules above all ask the same question: could this string escape the attribute it lands in? A @connect provider mark defeats that question, because a perfectly well-formed https:// URL beside the words “Connect Google” and a button is already the attack. There is no lexical rule that separates a real logo from a convincing one.

So @connect takes no img= (§4.18.5). The mark is chosen by the host, from a list keyed by provider=, and an unrecognised provider gets a neutral mark and a warning rather than a borrowed one. This is the only place in the language where an allowlist covers which value may be used rather than what shape it must have, and the reason is that the shape was never the risk.