For the complete documentation index, see llms.txt. This page is also available as Markdown.

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:

Stored and displayed as:

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:

Displayed as:

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:

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

Right:

The same applies to inline code:

<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

Python

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

Bash with jq

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:

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

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:


6. Images

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

  • 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.

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

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

Last updated