Why Lucy XSS Filter Turns Your Editor Shapes Into Plain Text
You load a PowerPoint file into Synap Editor, save the page through your server, and open it again later. Where a shape used to sit, you now see raw characters: <svg width="120" height="80"...>, right after a comment that reads <!-- Not Allowed Tag Filtered -->. If you try to rebuild the shape by hand, the size, rotation, and fill color are gone, because your server already dropped the attributes that held them. A video embedded next to it can go missing for the same reason.
Why your allowlist treats a shape as a threat
Your server almost certainly does not block content because it looks suspicious. It blocks content because the filter cannot find that tag or attribute's name on its list. That is how an allowlist works. It keeps the handful of names it recognizes and drops everything else, with nothing in between. Lucy XSS Filter follows that pattern in its lucy-xss-superset.xml configuration, and it checks every tag and every attribute by its exact name.
Two details make Lucy stricter than a quick read suggests. There is no wildcard for attributes, so a rule that allows data-width says nothing about data-height or any other data-* name. Each one needs its own entry. Lucy also inspects child tags, not just the top element, so permitting a div does not automatically permit the svg sitting inside it.
When a tag has no rule at all, Lucy does not hide it or swap in a placeholder. It turns the tag into escaped text instead. The characters <svg> become the literal text <svg in your saved HTML. Lucy puts a <!-- Not Allowed Tag Filtered --> comment right before it. An attribute with no rule is simply removed. Lucy leaves a <!-- Not Allowed Attribute Filtered (...) --> comment listing what it removed, unless your code creates the filter with XssFilter.getInstance(file, true), which drops all comments. Turning the filter off, or switching it to allow everything, is not an option once security policy requires it. The fix has to happen inside the allowlist itself.
Lucy drops any tag or attribute whose exact name is missing from the allowlist.
What a Synap Editor shape actually sends your server
To know which names to add, you need to know what Synap Editor writes to the page. A shape you draw, or one imported from a PowerPoint file, is saved as an SVG element wrapped in a <div class="se-shape">. Inside that wrapper you will find tags such as svg, g, path, defs, marker, polygon, ellipse, linearGradient, radialGradient, stop, pattern, and image. Typical attributes include viewBox, d, points, fill, stroke, stroke-width, and marker-end. None of those tag names appear in Lucy's default allowlist, which is why they come back as escaped text after a round trip through your server.
The second thing Synap Editor writes is less visible but just as important: the information needed to edit the shape again later, such as its size, rotation, fill, and stroke. That information lives in data- attributes, and almost all of them sit on the outer se-shape div rather than on the svg itself. The one exception is data-strokesize, which is attached to the svg element directly. When you reopen a page, Synap Editor reads data-shape-type first to decide what kind of shape to rebuild. Then it reads attributes like data-width, data-height, data-rotate, data-fill-color, and data-stroke-style to restore it. If Lucy strips those attributes while leaving the SVG tags alone, the shape still renders on screen, but it comes back as a flat picture instead of something you can resize or recolor. The same pattern applies to tables, memos, lists, and links, each carrying its own data- names on a specific element, not on whichever parent happens to be nearby.
Every name Lucy does not recognize is gone before the page reaches storage.
Add these exact names in a file that extends lucy-xss-superset.xml
Once you know the names, the fix is additive. Do not edit Lucy's own files. Create one file that extends lucy-xss-superset.xml, put it next to it on the classpath, and load it with XssFilter.getInstance("lucy-xss-synapeditor.xml"). Writing an element that Lucy already knows, such as div, adds to its existing rule instead of replacing it.
Three details decide whether it works. Lucy checks child tags by name, so div must allow svg as a child, and svg, g, defs and marker must list the drawing tags they contain. Every attribute you reference also needs its own line in attributeRule, or Lucy turns it off. And Lucy compares names without regard to case, so viewbox in the file also matches viewBox in your saved HTML.
<config xmlns="http://www.nhncorp.com/lucy-xss" extends="lucy-xss-superset.xml">
<elementRule>
<element name="div">
<attributes><ref name="data-shape-type"/><ref name="data-width"/><ref name="data-height"/><ref name="data-rotate"/></attributes>
<elements><ref name="svg"/></elements>
</element>
<element name="svg" endTag="true">
<attributes><ref name="Attrs"/><ref name="viewbox"/><ref name="preserveaspectratio"/></attributes>
<elements><ref name="g"/><ref name="path"/></elements>
</element>
<element name="g" endTag="true"><attributes><ref name="Attrs"/></attributes><elements><ref name="path"/></elements></element>
<element name="path" endTag="true"><attributes><ref name="Attrs"/><ref name="d"/><ref name="fill"/><ref name="stroke"/></attributes></element>
</elementRule>
<attributeRule>
<attribute name="data-shape-type"/><attribute name="data-width"/><attribute name="data-height"/><attribute name="data-rotate"/>
<attribute name="viewbox"/><attribute name="preserveaspectratio"/><attribute name="d"/><attribute name="fill"/><attribute name="stroke"/>
</attributeRule>
</config>
That is the core: a basic shape such as a rectangle or a line comes through with it. Arrows, gradients, connectors and video need a few more names, so the complete file is in Full configuration at the end.
We ran the complete file through Lucy 1.6.3 with a saved Synap Editor shape and a YouTube embed. Both came through unchanged, with every editing attribute intact. A <script> tag, an onclick attribute, an onload on svg, a javascript: address and an iframe from another site were still blocked.
Let video through without opening every iframe
An embedded video sits inside <span class="se-video-wrapper"> as an iframe. Lucy's superset configuration turns iframe off, so the complete file turns it back on. It also allows allowfullscreen and referrerpolicy. Synap Editor adds referrerpolicy="strict-origin-when-cross-origin" to every embed, and YouTube can refuse to play without it. title and style are already allowed by Lucy.
Turning iframe on must not let any page embed anything, and an allowedPattern on src cannot stop that. Lucy shares one src rule across every tag, and an allowed pattern only rescues a value that a not-allowed pattern caught, so an iframe pointing at any other site would pass. Instead, the file attaches a small element listener that keeps an iframe only when its src is one of the embed addresses Synap Editor creates. Compile it into your server code:
import com.nhncorp.lucy.security.xss.event.ElementListener;
import com.nhncorp.lucy.security.xss.markup.Element;
import java.util.regex.Pattern;
// Keeps an iframe only when its src is a Synap Editor video embed address.
public class VideoIframeListener implements ElementListener {
private static final Pattern ALLOWED = Pattern.compile(
"^https://(www\\.youtube\\.com/embed|player\\.vimeo\\.com/video|tv\\.naver\\.com/embed"
+ "|play-tv\\.kakao\\.com/embed/player|embed\\.ted\\.com)/.*");
public void handleElement(Element e) {
String src = e.getAttributeValue("src");
if (src == null || !ALLOWED.matcher(src.replaceAll("^[\"']|[\"']$", "")).matches()) {
e.setEnabled(false);
}
}
}
If you add sites with editor.upload.video.embedURLTransformers, add them to the listener's pattern too. Videos uploaded as files use video and track instead of iframe, and you allow them the same way you allowed the drawing tags.
Confirm the fix in your own saved HTML
After you update the allowlist, check your own saved HTML rather than trusting the configuration alone. Search it for the text Not Allowed Attribute Filtered, which marks an attribute Lucy removed. Also search for Not Allowed Tag Filtered and stray < text near your shape markup, which mark a tag Lucy escaped. If your code creates the filter without comments, only the escaped < text is left to find.
It is worth repeating that this allowlist work happens on your server, not in the editor. Synap Editor has its own content filter, with settings such as editor.contentFilter.allowIframe, allowEmbed, allowObject, allowScript, and allowLink, all off by default. getPublishingHtml() also drops script tags from the HTML it builds when script output is not allowed. None of that substitutes for Lucy. HTML can reach your server without ever passing through the editor, so whatever the editor strips or keeps on its own has no effect on content that arrives another way. Your Lucy configuration is the one place the policy is actually enforced.
The tag and attribute lists above came from reading Synap Editor's shape module, which is compiled, so treat them as a starting point rather than a final count. If a future release adds a tag or attribute not yet in your allowlist, the same filtered comment or escaped text will tell you what to add next.
Full configuration
Save this as lucy-xss-synapeditor.xml next to lucy-xss-superset.xml. It covers every shape tag and editing attribute Synap Editor writes, plus video embeds checked by the listener above.
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://www.nhncorp.com/lucy-xss" extends="lucy-xss-superset.xml">
<elementRule>
<!-- Shape wrapper: editing data on the div, svg allowed inside -->
<element name="div">
<attributes><ref name="ShapeData"/></attributes>
<elements><ref name="svg"/></elements>
</element>
<element name="svg" endTag="true">
<attributes><ref name="Attrs"/><ref name="viewbox"/><ref name="preserveaspectratio"/><ref name="data-strokesize"/><ref name="width"/><ref name="height"/></attributes>
<elements><ref name="SvgShapes"/></elements>
</element>
<element name="g" endTag="true"><attributes><ref name="Attrs"/><ref name="SvgPaint"/></attributes><elements><ref name="SvgShapes"/></elements></element>
<element name="defs" endTag="true"><attributes><ref name="Attrs"/></attributes><elements><ref name="SvgShapes"/></elements></element>
<element name="marker" endTag="true"><attributes><ref name="Attrs"/><ref name="SvgMarker"/></attributes><elements><ref name="SvgShapes"/></elements></element>
<element name="path" endTag="true"><attributes><ref name="Attrs"/><ref name="d"/><ref name="SvgPaint"/></attributes></element>
<element name="polygon" endTag="true"><attributes><ref name="Attrs"/><ref name="points"/><ref name="SvgPaint"/></attributes></element>
<element name="ellipse" endTag="true"><attributes><ref name="Attrs"/><ref name="cx"/><ref name="cy"/><ref name="rx"/><ref name="ry"/><ref name="SvgPaint"/></attributes></element>
<element name="lineargradient" endTag="true"><attributes><ref name="Attrs"/><ref name="x1"/><ref name="y1"/><ref name="x2"/><ref name="y2"/></attributes><elements><ref name="stop"/></elements></element>
<element name="radialgradient" endTag="true"><attributes><ref name="Attrs"/><ref name="cx"/><ref name="cy"/><ref name="r"/></attributes><elements><ref name="stop"/></elements></element>
<element name="stop" endTag="true"><attributes><ref name="Attrs"/><ref name="offset"/><ref name="stop-color"/></attributes></element>
<element name="pattern" endTag="true"><attributes><ref name="Attrs"/><ref name="patternunits"/><ref name="width"/><ref name="height"/></attributes><elements><ref name="image"/></elements></element>
<element name="image" endTag="true"><attributes><ref name="Attrs"/><ref name="href"/><ref name="width"/><ref name="height"/></attributes></element>
<!-- Video: turn iframe back on; the listener keeps only Synap Editor embed addresses -->
<element name="iframe" disable="false" endTag="true">
<attributes><ref name="Attrs"/><ref name="src"/><ref name="title"/><ref name="allowfullscreen"/><ref name="referrerpolicy"/></attributes>
<listener>VideoIframeListener</listener>
</element>
</elementRule>
<elementGroup name="SvgShapes">
<ref name="g"/><ref name="path"/><ref name="defs"/><ref name="marker"/><ref name="polygon"/><ref name="ellipse"/>
<ref name="lineargradient"/><ref name="radialgradient"/><ref name="pattern"/><ref name="image"/>
</elementGroup>
<attributeRule>
<attribute name="data-shape-type"/><attribute name="data-width"/><attribute name="data-height"/><attribute name="data-rotate"/><attribute name="data-flip-h"/><attribute name="data-flip-v"/>
<attribute name="data-fill-color"/><attribute name="data-stroke-fill-color"/><attribute name="data-stroke-style"/><attribute name="data-stroke-width"/><attribute name="data-marker-head"/><attribute name="data-marker-tail"/>
<attribute name="data-gradient-fill"/><attribute name="data-adj-value"/><attribute name="data-image-path"/><attribute name="data-start-connection"/><attribute name="data-end-connection"/><attribute name="data-connection-id"/>
<attribute name="data-strokesize"/><attribute name="viewbox"/><attribute name="preserveaspectratio"/><attribute name="d"/><attribute name="points"/>
<attribute name="fill"/><attribute name="stroke"/><attribute name="stroke-width"/><attribute name="stroke-dasharray"/><attribute name="vector-effect"/><attribute name="marker-start"/><attribute name="marker-end"/>
<attribute name="markerwidth"/><attribute name="markerheight"/><attribute name="markerunits"/><attribute name="refx"/><attribute name="refy"/><attribute name="orient"/>
<attribute name="cx"/><attribute name="cy"/><attribute name="r"/><attribute name="rx"/><attribute name="ry"/><attribute name="x1"/><attribute name="y1"/><attribute name="x2"/><attribute name="y2"/>
<attribute name="offset"/><attribute name="stop-color"/><attribute name="patternunits"/><attribute name="allowfullscreen"/><attribute name="referrerpolicy"/>
</attributeRule>
<attributeGroup name="ShapeData">
<ref name="data-shape-type"/><ref name="data-width"/><ref name="data-height"/><ref name="data-rotate"/><ref name="data-flip-h"/><ref name="data-flip-v"/>
<ref name="data-fill-color"/><ref name="data-stroke-fill-color"/><ref name="data-stroke-style"/><ref name="data-stroke-width"/><ref name="data-marker-head"/><ref name="data-marker-tail"/>
<ref name="data-gradient-fill"/><ref name="data-adj-value"/><ref name="data-image-path"/><ref name="data-start-connection"/><ref name="data-end-connection"/><ref name="data-connection-id"/>
</attributeGroup>
<attributeGroup name="SvgPaint">
<ref name="fill"/><ref name="stroke"/><ref name="stroke-width"/><ref name="stroke-dasharray"/><ref name="vector-effect"/><ref name="marker-start"/><ref name="marker-end"/>
</attributeGroup>
<attributeGroup name="SvgMarker">
<ref name="markerwidth"/><ref name="markerheight"/><ref name="markerunits"/><ref name="refx"/><ref name="refy"/><ref name="orient"/><ref name="viewbox"/>
</attributeGroup>
</config>
Common questions
Does disabling Lucy fix the shape problem?
It removes the symptom, but not in a way a security policy that requires the filter will accept. The fix that keeps the filter on is adding the exact tag and attribute names Synap Editor uses in a file that extends lucy-xss-superset.xml, rather than turning the filter off.
Could a future Synap Editor release need more allowlist entries?
Yes. The lists in this guide came from reading a compiled shape module, so they may not be complete. Check your saved HTML for filtered comments or escaped tags like <svg after any upgrade, and add whatever name shows up.
Can I allow video iframes without allowing every iframe?
Yes, but not with a src pattern. Lucy shares one src rule across every tag, so an allowedPattern cannot limit iframes to certain sites. Turn iframe on and attach the element listener shown above, which keeps an iframe only when its src is a known embed address. Keep allowfullscreen and referrerpolicy on the allowlist so video still plays.
Does the editor's own content filter replace a server filter?
No. Settings like editor.contentFilter.allowIframe and the script removal in getPublishingHtml() only affect HTML that passes through the editor, and HTML can reach your server without it. Lucy remains the filter that actually enforces policy.
Is Lucy XSS Filter still maintained upstream? Its GitHub repository is archived, and the project page points to OWASP Java HTML Sanitizer as a newer alternative. That does not change anything in the configuration above: if you are already running Lucy, the allowlist entries in this guide keep working.