> For the complete documentation index, see [llms.txt](https://support.attackforge.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://support.attackforge.com/app/getting-started/working-with-rich-text-fields.md).

# Working With Rich-Text Fields

## The AttackForge Rich-Text Format

Many AttackForge fields — vulnerability descriptions, attack scenarios, remediation recommendations, steps to reproduce, notes, executive summaries, test case details, and any custom field configured as **Rich-Text** — store formatted content rather than plain text. AttackForge calls these *rich-text* fields.

A rich-text field holds **HTML from a fixed, restricted subset**. That subset is the same everywhere: whether the content arrives from the rich text editor in the web application, from the Self-Service API (SSAPI), or from the MCP server, it is held to the same rules and rendered by the same component.

This matters most when you write rich-text through the **SSAPI**, because there you supply the markup yourself. Content outside the supported subset is removed before it reaches the screen. The removal is silent — the API accepts your request, returns success, and the missing content only becomes apparent when someone opens the record or exports a report.

> **The single most common problem:** a proof of concept containing angle brackets — a payload, an XML fragment, a placeholder — disappears from the finding. The fix is HTML escaping, and it is covered in full below.

***

### 1. You are sending HTML, not text

When you `POST` a rich-text value, the string you send is parsed as HTML. This has two consequences that catch people out.

**Anything that looks like a tag is treated as one.** `<script>`, `<img …>`, `<soap:Envelope>` and even `<username>` are all parsed as markup. If the tag is not on the supported list, it is deleted.

**AttackForge also derives a plain-text copy** of every rich-text field. That copy is what feeds report exports, Word and PDF output, and search. It is produced by converting your HTML to text — so markup that was discarded is missing from your reports as well as from the screen.

Both effects point the same way: content must be expressed using the supported subset, and literal text that contains `<`, `>` or `&` must be escaped.

#### Rich-text or plain text?

Some API fields are paired with a type discriminator — for example a note may be sent alongside a `note_type`, and a test case execution flow step alongside a `details_type`. The accepted values are `RICHTEXT` and `PLAINTEXT`, and the value decides whether your string is interpreted as markup at all.

If you are sending HTML, set the discriminator to `RICHTEXT`. Check the API reference for which fields on a given endpoint accept rich-text and which carry a discriminator; this article covers the format itself rather than the field list.

***

### 2. Escaping — the part that matters most

#### 2.1 The three characters

Before you place any literal text into rich-text, convert these characters to HTML entities:

| Character | Entity   | Notes                                                                                         |
| --------- | -------- | --------------------------------------------------------------------------------------------- |
| `&`       | `&amp;`  | **Convert this first**, otherwise you will re-escape the `&` in the entities you produce next |
| `<`       | `&lt;`   | Always. There is no safe context for a bare `<`                                               |
| `>`       | `&gt;`   | Always                                                                                        |
| `"`       | `&quot;` | Required inside attribute values; harmless elsewhere                                          |

Order matters. `&` must be replaced before `<` and `>`, or `<` becomes `&lt;` and then the `&` in `&lt;` becomes `&amp;lt;`, and your reader sees the literal text `&lt;`.

#### 2.2 This is not a `<script>` problem — it is an angle-bracket problem

It is tempting to think of escaping as a workaround for XSS payloads. It is not. **Every** tag outside the supported subset is deleted, whatever it is and whatever it means to you.

Where the tag wraps text, the text survives and the tags vanish. Where the content *is* the tag — which is true of most single-tag payloads — nothing at all is left behind:

| What you send inside a `<p>`                                        | What is stored and displayed     |
| ------------------------------------------------------------------- | -------------------------------- |
| `Payload: <img src=x onerror=alert(1)> reflected.`                  | `Payload: reflected.`            |
| `Payload: <svg onload=alert(1)> reflected.`                         | `Payload: reflected.`            |
| `Payload: <iframe src="javascript:alert(1)"></iframe> reflected.`   | `Payload: reflected.`            |
| `Sent <soap:Envelope><soap:Body/></soap:Envelope> to the endpoint.` | `Sent to the endpoint.`          |
| `Try <%= 7*7 %> in the name field.`                                 | `Try in the name field.`         |
| `Filter: (cn=<user>) applied.`                                      | `Filter: (cn=) applied.`         |
| `Generic: List<String> used throughout.`                            | `Generic: List used throughout.` |
| `Payload: <div>inner text</div> reflected.`                         | `Payload: inner text reflected.` |

Note the last four rows. A SOAP body, a template-injection probe, an LDAP filter with a placeholder, and a Java generic are not payloads in any meaningful sense — they are ordinary technical prose, and they are destroyed just the same.

`<script>` is the worst case rather than a special case: it is the one tag whose **inner text** is removed along with the tag itself.

| What you send inside a `<p>`                     | What is stored and displayed    |
| ------------------------------------------------ | ------------------------------- |
| `Payload: <script>alert(1);</script> reflected.` | `Payload: reflected.`           |
| `Payload: <div>alert(1);</div> reflected.`       | `Payload: alert(1); reflected.` |

#### 2.3 The fix, end to end

Escape the literal text, then place it in your markup.

**Wrong** — the payload is parsed as a tag and removed, including its contents:

```html
<p>The search parameter reflected <script>alert(1);</script> without encoding.</p>
```

Stored and displayed as:

```
The search parameter reflected  without encoding.
```

The exported plain-text copy is equally damaged: `The search parameter reflected without encoding.`

**Right** — the payload is escaped, so it is text rather than markup:

```html
<p>The search parameter reflected &lt;script&gt;alert(1);&lt;/script&gt; without encoding.</p>
```

Displayed as:

```
The search parameter reflected <script>alert(1);</script> without encoding.
```

And the exported plain-text copy reads `The search parameter reflected <script>alert(1);</script> without encoding.` — correct in both places.

#### 2.4 Escaping inside code blocks and inline code

`<pre>` and `<code>` change how content is *displayed*; they do not change how it is *parsed*. Markup inside them is still markup, and still removed. Escape there too — in practice, escape there especially, because that is where payloads normally live.

**Wrong:**

```html
<pre>GET /search?q=<script>alert(1)</script> HTTP/1.1
Host: example.com</pre>
```

Stored as `GET /search?q= HTTP/1.1` — the payload is gone from the request you were documenting.

**Right:**

```html
<pre>GET /search?q=&lt;script&gt;alert(1)&lt;/script&gt; HTTP/1.1
Host: example.com</pre>
```

The same applies to inline code:

```html
<p>The parameter accepts <code class="inline-code-container">&lt;svg onload=alert(1)&gt;</code> unfiltered.</p>
```

`<pre>` is the one place where line breaks and indentation are preserved exactly as you send them. Everywhere else, whitespace between tags is insignificant.

#### 2.5 Escaping is safe to apply, and safe to apply twice

Escaped content is stable. Sending `&lt;script&gt;` stores `&lt;script&gt;`; it is never double-escaped into `&amp;lt;script&amp;gt;`, and it survives a round trip through the web application's editor unchanged. You can escape confidently without worrying about accumulating entities on each save.

If you genuinely want the reader to see the *entity* `&lt;` rather than a `<` character, escape the ampersand yourself and send `&amp;lt;`.

Numeric character references work as well as named ones: `&#60;` and `&#x3C;` are both accepted alternatives to `&lt;`.

#### 2.6 Never rely on a bare `<`

A bare `<` in your text is a coin toss. Whether it is escaped for you or eaten depends on whether the parser can find something tag-shaped after it:

| What you send                | What is stored                                             |
| ---------------------------- | ---------------------------------------------------------- |
| `<p>1 < 2</p>`               | `<p>1 &lt; 2</p>` — escaped for you                        |
| `<p>Tom & Jerry < 5 > 3</p>` | `<p>Tom & Jerry 3</p>` — `< 5 >` read as a tag and deleted |
| `<p>a > b</p>`               | `<p>a &gt; b</p>` — a stray `>` is escaped for you         |

Do not depend on the forgiving cases. Escape every `<` and `>` in literal text and the outcome is never in question.

#### 2.7 Building the request body safely

Escape the text, interpolate it into your markup, then let your JSON library serialise the result. **HTML escaping and JSON escaping are separate layers** — your HTTP client handles the JSON layer; only you can handle the HTML layer.

**JavaScript / TypeScript**

```js
const escapeHtml = (s) =>
  s.replace(/&/g, '&amp;')
   .replace(/</g, '&lt;')
   .replace(/>/g, '&gt;')
   .replace(/"/g, '&quot;');

const payload = `<script>alert(1);</script>`;
const richtext = `<p>The search parameter reflected ${escapeHtml(payload)} without encoding.</p>`;

await fetch('https://your-tenant.attackforge.com/api/ss/vulnerability', {
  method: 'POST',
  headers: { 'X-SSAPI-KEY': apiKey, 'Content-Type': 'application/json' },
  body: JSON.stringify({ steps_to_reproduce: richtext, /* … */ }),
});
```

**Python**

```python
import html, requests

payload = "<script>alert(1);</script>"
richtext = f"<p>The search parameter reflected {html.escape(payload)} without encoding.</p>"

requests.post(
    "https://your-tenant.attackforge.com/api/ss/vulnerability",
    headers={"X-SSAPI-KEY": api_key},
    json={"steps_to_reproduce": richtext},
)
```

`html.escape` handles `&`, `<`, `>` and quotes in the correct order.

**Bash with `jq`**

```bash
payload='<script>alert(1);</script>'
escaped=$(printf '%s' "$payload" | sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g')
richtext="<p>The search parameter reflected ${escaped} without encoding.</p>"

jq -n --arg r "$richtext" '{steps_to_reproduce: $r}' \
  | curl -sS -X POST "https://your-tenant.attackforge.com/api/ss/vulnerability" \
      -H "X-SSAPI-KEY: $API_KEY" -H 'Content-Type: application/json' -d @-
```

Substituting `&` first is what makes the `sed` chain correct; reordering it produces `&amp;lt;`.

#### 2.8 What not to do instead

None of the following preserve your content. Escape with HTML entities — there is no alternative mechanism:

* **`<![CDATA[ … ]]>`** — an XML construct with no meaning in HTML. The payload inside it is still removed, and the `<![CDATA[` and `]]>` markers are left on screen around the gap.
* **Backslash escaping** (`\<script\>`) — the backslashes are displayed literally and the tag is still removed.
* **Base64 or URL encoding the payload** — the reader sees an encoded blob rather than the payload.
* **Zero-width or lookalike characters** inserted to break up the tag — they corrupt the payload for anyone who copies it out to retest.
* **Wrapping the payload in `<pre>` or `<code>` without escaping** — as shown in section 2.4, this does not help.

***

### 3. Supported tags

Only the tags in this table are stored. Everything else is removed.

| Tag                  | Purpose               | Attributes                                                                      | Example                                                                            |
| -------------------- | --------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `<h1>` `<h2>` `<h3>` | Headings              | None                                                                            | `<h2>Steps to Reproduce</h2>`                                                      |
| `<p>`                | Paragraph             | None                                                                            | `<p>Body text.</p>`                                                                |
| `<pre>`              | Code block            | None                                                                            | `<pre>const num = 100;</pre>`                                                      |
| `<blockquote>`       | Block quote           | None                                                                            | `<blockquote>Observed 2026-05-01.</blockquote>`                                    |
| `<ol>`               | Numbered list         | None                                                                            | `<ol><li>first</li><li>second</li></ol>`                                           |
| `<ul>`               | Bulleted list         | None                                                                            | `<ul><li>one</li><li>two</li></ul>`                                                |
| `<li>`               | List item             | None; valid only inside `<ol>` or `<ul>`                                        | `<li>item text</li>`                                                               |
| `<table>`            | Table                 | `style` (required) — see section 5                                              | see section 5                                                                      |
| `<tbody>`            | Table body            | None; valid only inside `<table>`                                               | `<tbody>…</tbody>`                                                                 |
| `<tr>`               | Table row             | `style` (optional, `height` only)                                               | `<tr style="height:40px;">…</tr>`                                                  |
| `<td>`               | Table cell            | `style` (required) — see section 5                                              | `<td style="min-width:100px;"><p>cell</p></td>`                                    |
| `<br>`               | Empty table cell only | None; valid only as `<p><br></p>` inside a `<td>`                               | `<p><br></p>`                                                                      |
| `<strong>`           | Bold                  | None                                                                            | `<strong>bold</strong>`                                                            |
| `<em>`               | Italic                | None                                                                            | `<em>italic</em>`                                                                  |
| `<u>`                | Underline             | None                                                                            | `<u>underline</u>`                                                                 |
| `<s>`                | Strikethrough         | None                                                                            | `<s>struck</s>`                                                                    |
| `<span>`             | Highlight             | `data-highlight` (required): `orange`, `red`, `green`, `blue` or `violet`       | `<span data-highlight="red">Critical</span>`                                       |
| `<code>`             | Inline code           | `class` (required): `inline-code-container`                                     | `<code class="inline-code-container">q</code>`                                     |
| `<a>`                | Hyperlink             | `href` (http/https), `rel="noopener noreferrer"`, `target="_blank"` — all three | `<a href="https://example.com" rel="noopener noreferrer" target="_blank">link</a>` |

Use `<strong>` and `<em>` rather than `<b>` and `<i>`. Bare `<span>` and bare `<code>` are not supported — both require their attribute.

***

### 4. Structure rules

**Send minified HTML.** No newlines, tabs or indentation between tags. Whitespace *within* visible text is preserved, and whitespace inside `<pre>` is preserved exactly; everywhere else it is insignificant, and pretty-printed markup will not survive as you formatted it.

**There is no line-break tag.** Separate lines are separate `<p>` elements. `<br>` has exactly one valid use: representing an empty table cell as `<p><br></p>`.

**Block tags do not nest.** `<h1>`, `<h2>`, `<h3>`, `<p>`, `<pre>`, `<blockquote>`, `<ol>`, `<ul>` and `<table>` are block-level and must not contain one another, except that `<ol>`/`<ul>` contain `<li>`, and table cells may contain `<p>` and lists.

**Lists do not nest.** `<ol>` and `<ul>` may contain only `<li>` elements, and an `<li>` may not contain another list.

**Inline tag placement:**

| Container            | Inline tags allowed      |
| -------------------- | ------------------------ |
| `<p>`                | All                      |
| `<li>`               | All                      |
| `<h1>` `<h2>` `<h3>` | None — plain text only   |
| `<blockquote>`       | None — plain text only   |
| `<pre>`              | `<span>` highlights only |

**Emphasis combines; the others do not.** `<strong>`, `<em>`, `<u>` and `<s>` may be nested within one another. Highlight spans, `<code>` and `<a>` may not be combined with emphasis tags or with each other — their content cannot be styled.

***

### 5. Tables

Structure is fixed and must nest in exactly this order:

```
<table> → <tbody> → <tr> → <td>
```

There are no header cells. `<thead>` and `<th>` are not supported — a header row is an ordinary `<tr>` of `<td>` elements whose content you make bold. If you send `<thead>`/`<th>` anyway, the tags are stripped and the header text collapses out of the table structure.

#### Widths

`<table>` and `<td>` each require a `style` attribute carrying a single width property, and a table must use one of two modes consistently:

**Default mode — no explicit widths.** Every `<td>` uses `style="min-width:100px;"`, and `<table>` uses `style="min-width:Xpx;"` where X is the sum of the `min-width` values of the cells in one row.

**Explicit mode — you specify a width for any column.** The whole table converts to `width`. Every `<td>` uses `style="width:Ypx;"` — your value for that column, or `100px` for columns you did not specify — and `<table>` uses `style="width:Xpx;"` where X is the sum of the row's cell widths. No cell keeps `min-width`.

Rules that apply to both modes:

* `min-width` and `width` are the only permitted style properties, never both on the same element.
* A single table must not mix `min-width` cells with `width` cells.
* Values must be in pixels. Percentages and other units are rejected.
* **Use two or more digits.** `100px` and `10px` are accepted; `5px` is rejected and the whole style attribute is dropped.
* Omitting the `style` attribute is invalid.

#### Rows

`<tr>` accepts an optional `style` carrying only a `height` in pixels, e.g. `<tr style="height:40px;">`. Include it only when you want a specific row height; otherwise omit the attribute entirely. Row height is independent of column widths and works with either mode.

#### Cell contents

A `<td>` may contain one or more `<p>` elements and `<ol>`/`<ul>` lists. Cell paragraphs and list items follow the normal inline rules, highlights included.

Not allowed inside a `<td>`: code blocks (`<pre>`), image tokens, headings, blockquotes, and nested tables.

An empty cell is written as `<p><br></p>`.

#### Example

```html
<table style="min-width:200px;"><tbody><tr><td style="min-width:100px;"><p><strong>Field</strong></p></td><td style="min-width:100px;"><p><strong>Detail</strong></p></td></tr><tr><td style="min-width:100px;"><p>Parameter</p></td><td style="min-width:100px;"><p><code class="inline-code-container">q</code></p></td></tr><tr><td style="min-width:100px;"><p>Notes</p></td><td style="min-width:100px;"><p><br></p></td></tr></tbody></table>
```

With explicit widths of 250px and 120px, and a third column left unspecified — note that every cell converts to `width`, and the table width is 250 + 120 + 100 = 470:

```html
<table style="width:470px;"><tbody><tr><td style="width:250px;"><p>api.example.com</p></td><td style="width:120px;"><p>443</p></td><td style="width:100px;"><p><br></p></td></tr></tbody></table>
```

***

### 6. Images

There is no `<img>` tag in rich-text. Images are referenced by a token naming a file that already exists in AttackForge:

```html
<p>{{{login-bypass.png}}}</p>
```

* Three braces on each side, wrapped around the filename.
* The filename must match an uploaded file exactly. Upload the file to the record first, then reference it by name.
* The token must be the only content of its `<p>` — no text, whitespace or other tags beside it. `<p>See {{{login-bypass.png}}}</p>` will not render as an image.
* Do not escape anything inside the token; the braces and filename are literal.
* Image tokens are not permitted inside table cells.

***

### 7. What gets removed

#### Deleted entirely, contents and all

| Sent                                                           | Result                         |
| -------------------------------------------------------------- | ------------------------------ |
| `<script>…</script>`                                           | Tag and its inner text removed |
| `<img>`, `<svg>`, `<iframe>`, `<video>`, `<object>`, `<embed>` | Removed                        |
| Arbitrary XML (`<soap:Envelope>`, `<Request>`, …)              | Removed                        |
| Template syntax read as a tag (`<%= … %>`)                     | Removed                        |
| Placeholders in prose (`<username>`, `List<String>`)           | Removed                        |

#### Kept, but altered

| Sent                                                                      | Result                                                                       |
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `<div>`, `<section>`, `<style>`, `<h4>`–`<h6>`, `<sub>`, `<sup>`          | Tags removed, inner text kept                                                |
| `<b>` / `<i>`                                                             | Not supported — use `<strong>` / `<em>`                                      |
| `<thead>` / `<th>`                                                        | Removed; header text falls out of the table structure                        |
| `onclick` and other event handlers                                        | Attribute removed, element kept                                              |
| `href="javascript:…"`                                                     | Link emptied                                                                 |
| `<a>` missing `rel` or `target`                                           | Link stored without the missing behaviour — always send all three attributes |
| `style` properties other than `min-width`/`width` (or `height` on `<tr>`) | Property dropped                                                             |
| `min-width:5px`, `width:50%`                                              | Rejected; the entire style attribute is dropped                              |
| `data-highlight="pink"`                                                   | Stored but rendered unstyled — use one of the five supported colours         |
| `<span>` without `data-highlight`, `<code>` without its class             | Stored, but renders as unstyled plain text — always include the attribute    |
| Markdown syntax (`**bold**`, `# heading`, `- item`)                       | Displayed as literal characters — Markdown is never interpreted              |

***

### 8. Worked example

An SSAPI request body containing a heading, a numbered list, an escaped payload in prose, an escaped HTTP request in a code block, a highlight, a link, a table and an image reference. Note both escaping layers: `&lt;` for HTML, and `\"` / `\n` for JSON.

```json
{
  "steps_to_reproduce": "<h2>Steps to Reproduce</h2><ol><li>Browse to the search page.</li><li>Submit <code class=\"inline-code-container\">&lt;script&gt;alert(1);&lt;/script&gt;</code> in the <strong>q</strong> parameter.</li><li>Observe the script execute in the response.</li></ol><h2>Request</h2><pre>GET /search?q=&lt;script&gt;alert(1);&lt;/script&gt; HTTP/1.1\nHost: example.com</pre><p>The parameter is reflected without encoding, so severity is <span data-highlight=\"red\">Critical</span>. See the <a href=\"https://owasp.org/www-community/attacks/xss/\" rel=\"noopener noreferrer\" target=\"_blank\">OWASP XSS</a> page.</p><h2>Evidence</h2><p>{{{reflected-xss.png}}}</p><h2>Summary</h2><table style=\"min-width:200px;\"><tbody><tr><td style=\"min-width:100px;\"><p><strong>Field</strong></p></td><td style=\"min-width:100px;\"><p><strong>Detail</strong></p></td></tr><tr><td style=\"min-width:100px;\"><p>Parameter</p></td><td style=\"min-width:100px;\"><p><code class=\"inline-code-container\">q</code></p></td></tr><tr><td style=\"min-width:100px;\"><p>Notes</p></td><td style=\"min-width:100px;\"><p><br></p></td></tr></tbody></table>"
}
```

Which renders as:

> ### Steps to Reproduce
>
> 1. Browse to the search page.
> 2. Submit `<script>alert(1);</script>` in the **q** parameter.
> 3. Observe the script execute in the response.
>
> ### Request
>
> ```
> GET /search?q=<script>alert(1);</script> HTTP/1.1
> Host: example.com
> ```
>
> The parameter is reflected without encoding, so severity is Critical (highlighted red). See the OWASP XSS page.
>
> ### Evidence
>
> *(the uploaded image `reflected-xss.png`)*
>
> ### Summary

| Field     | Detail |
| --------- | ------ |
| Parameter | `q`    |
| Notes     |        |

Field names differ between endpoints — check the API reference for the endpoint you are calling. The format of the value is the same everywhere.

***

### 9. Checklist before you send

* [ ] Every `&`, `<` and `>` in literal text is escaped — `&` first.
* [ ] Payloads, XML fragments and placeholders are escaped, including inside `<pre>` and `<code>`.
* [ ] Only supported tags are used; no `<div>`, `<img>`, `<b>`, `<i>`, `<thead>` or `<th>`.
* [ ] No Markdown syntax anywhere.
* [ ] The markup is minified — no newlines or indentation between tags, except inside `<pre>`.
* [ ] Line breaks are separate `<p>` elements; `<br>` appears only as `<p><br></p>` in a table cell.
* [ ] Block tags are not nested, and lists are not nested.
* [ ] Every `<table>` and `<td>` carries a `style` width; one mode throughout; pixel values of two or more digits; the table width equals the sum of one row's cell widths.
* [ ] `<a>` elements carry `href`, `rel="noopener noreferrer"` and `target="_blank"`.
* [ ] Highlights use `orange`, `red`, `green`, `blue` or `violet`.
* [ ] Image tokens name uploaded files, sit alone in their own `<p>`, and are not inside a table.
* [ ] Where the field has a type discriminator, it is set to `RICHTEXT`.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://support.attackforge.com/app/getting-started/working-with-rich-text-fields.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
