What installation are you running?
Production (netalertx) 📦
Is there an existing issue for this?
The issue occurs in the following browsers. Select at least 2.
Current Behavior
If any device's devComments (or likely devName/devOwner - not individually tested, but they flow through the same unescaped path) contains a bare < that isn't valid XML markup, the entire notification build crashes with yattag.indentation.XMLTokenError - not just the one device's row, the whole process. This happens in notification.create(), well after all sections have already been assembled ("events section done." etc. all log successfully) - the crash is specifically in the final indent() call that pretty-prints the whole assembled HTML document.
Severity: this kills the entire main() process for that cycle via an unhandled exception, and since the crash happens before any event's evePendingAlertEmail flag gets cleared, the same poisoned pending event is picked up again on the next cycle and crashes identically - a permanent, recurring failure for every pending notification (not just the one with the bad comment) until someone manually clears or edits the offending row in the database. In my case this device was Meta Quest Pro, but any device is equally capable of triggering this via a normal free-text comment.
One thing worth flagging about the error message itself, since it's actively misleading: XMLTokenError's message is built from repr(string[:100]) - the first 100 characters of the entire document being tokenized, not the actual failing position. tokenize()'s regex match happens at an offset (start) into the original string, but the exception only ever shows string[:100], so the reported snippet is always just the document's opening HTML regardless of where the real problem is. In my case this pointed at an unrelated <span> near the top of the template, not the actual malformed content, which cost real time chasing the wrong lead before rebuilding the exact HTML locally with proper position tracking to find the real offset.
Expected Behavior
A single device's free-text field containing ordinary punctuation (parentheses, a <= comparison, anything a person might reasonably type into a comment box) should never be able to crash notification generation for the entire installation. At minimum, free-text fields should be HTML-escaped before being embedded into the notification's HTML; ideally the indent() call should also be resilient to a single malformed fragment (e.g. catch XMLTokenError and fall back to sending the unindented HTML rather than losing the whole notification).
Steps To Reproduce
Reproduced in an isolated Docker container (ghcr.io/jokob-sk/netalertx:latest, same image as production - confirmed on v26.8.5), separate from any production data, via the real scheduler (not manually invoked).
- Fresh NetAlertX instance.
LOG_LEVEL='trace'.
- Insert one device with a
devComments value containing a bare < followed by something that isn't a valid tag name - e.g. Test comment with a bare less-than: values <=2 break things - and one pending event for it (evePendingAlertEmail=1):
INSERT INTO Devices (devMac, devName, devOwner, devType, devVendor, devFirstConnection, devLastConnection, devLastIP, devStaticIP, devPresentLastScan, devIsArchived, devParentMAC, devParentRelType, devComments)
VALUES ('aa:bb:cc:dd:ee:ff', 'ReproDevice', '', '', 'TestVendor', datetime('now'), datetime('now'), '192.168.1.99', 0, 1, 0, '', 'default', 'Test comment with a bare less-than: values <=2 break things');
INSERT INTO Events (eveMac, eveIp, eveDateTime, eveEventType, evePendingAlertEmail)
VALUES ('aa:bb:cc:dd:ee:ff', '192.168.1.99', datetime('now', '-1 minutes'), 'Disconnected', 1);
- Do not call any notification logic manually - just wait for the app's own normal scheduler to run the next notification check.
Result: the process crashes with the traceback below on the very next cycle. evePendingAlertEmail for the test event is still 1 afterward (confirmed via the same query) - the crash happens before it would ever be cleared, so this recurs on every subsequent cycle unmodified.
Relevant app.conf settings
TIMEZONE='Europe/Berlin'
ARPSCAN_RUN='schedule'
LOG_LEVEL='trace'
docker-compose.yml
services:
netalertx:
image: ghcr.io/jokob-sk/netalertx:latest
container_name: netalertx-repro-htmlescape
ports:
- "11035:1031"
- "10215:20212"
environment:
TZ: Europe/Warsaw
PORT: 1031
GRAPHQL_PORT: 20212
volumes:
- ./data:/data
(Isolated repro - bridge networking instead of production's `network_mode: host`, distinct ports so it doesn't collide with a real instance on the same host. Unlike the previous two isolated repros, no `cap_add` flags were needed here - the container started and ran normally without them for this specific test.)
Debug or Trace enabled
Relevant app.log section
11:50:05 [Notification] Check if something to report
11:50:05 [Notification] Included sections: ['new_devices', 'down_devices', 'events']
11:50:05 [Notification] Open text Template
11:50:05 [Notification] Open html Template
11:50:05 [Notification] new_devices section done.
11:50:05 [Notification] down_devices section done.
11:50:05 [Notification] down_reconnected section done.
11:50:05 [Notification] events section done.
11:50:05 [Notification] plugins section done.
Traceback (most recent call last):
File "<frozen runpy>", line 198, in _run_module_as_main
File "<frozen runpy>", line 88, in _run_code
File "/app/server/__main__.py", line 291, in <module>
sys.exit(main())
^^^^^^
File "/app/server/__main__.py", line 197, in main
notificationObj = notification.create(final_json, "")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/server/models/notification_instance.py", line 160, in create
final_html = indent(
^^^^^^^
File "/opt/venv/lib/python3.12/site-packages/yattag/indentation.py", line 326, in indent
tokens = tokenize(string)
^^^^^^^^^^^^^^^^
File "/opt/venv/lib/python3.12/site-packages/yattag/indentation.py", line 180, in tokenize
raise XMLTokenError("Unrecognized XML token near %s" % repr(string[:100]))
yattag.indentation.XMLTokenError: Unrecognized XML token near '<html>\n<head></head>\n<body>\n <span style="display:none !important;visibility:hidden;mso-hide:all;'
Note on that last line: as explained above, string[:100] here is misleading - it's just the start of the whole document, not where the actual bad token is. In this run the real failure was 3375 characters in, landing exactly inside the test device's devComments text (confirmed by rebuilding the exact same HTML locally and re-running tokenize() with real position tracking instead of yattag's own truncated message).
Docker Logs
netalertx-bug3-docker-logs.txt
What installation are you running?
Production (netalertx) 📦
Is there an existing issue for this?
The issue occurs in the following browsers. Select at least 2.
Current Behavior
If any device's
devComments(or likelydevName/devOwner- not individually tested, but they flow through the same unescaped path) contains a bare<that isn't valid XML markup, the entire notification build crashes withyattag.indentation.XMLTokenError- not just the one device's row, the whole process. This happens innotification.create(), well after all sections have already been assembled ("events section done." etc. all log successfully) - the crash is specifically in the finalindent()call that pretty-prints the whole assembled HTML document.Severity: this kills the entire
main()process for that cycle via an unhandled exception, and since the crash happens before any event'sevePendingAlertEmailflag gets cleared, the same poisoned pending event is picked up again on the next cycle and crashes identically - a permanent, recurring failure for every pending notification (not just the one with the bad comment) until someone manually clears or edits the offending row in the database. In my case this device wasMeta Quest Pro, but any device is equally capable of triggering this via a normal free-text comment.One thing worth flagging about the error message itself, since it's actively misleading:
XMLTokenError's message is built fromrepr(string[:100])- the first 100 characters of the entire document being tokenized, not the actual failing position.tokenize()'s regex match happens at an offset (start) into the original string, but the exception only ever showsstring[:100], so the reported snippet is always just the document's opening HTML regardless of where the real problem is. In my case this pointed at an unrelated<span>near the top of the template, not the actual malformed content, which cost real time chasing the wrong lead before rebuilding the exact HTML locally with proper position tracking to find the real offset.Expected Behavior
A single device's free-text field containing ordinary punctuation (parentheses, a
<=comparison, anything a person might reasonably type into a comment box) should never be able to crash notification generation for the entire installation. At minimum, free-text fields should be HTML-escaped before being embedded into the notification's HTML; ideally theindent()call should also be resilient to a single malformed fragment (e.g. catchXMLTokenErrorand fall back to sending the unindented HTML rather than losing the whole notification).Steps To Reproduce
Reproduced in an isolated Docker container (
ghcr.io/jokob-sk/netalertx:latest, same image as production - confirmed on v26.8.5), separate from any production data, via the real scheduler (not manually invoked).LOG_LEVEL='trace'.devCommentsvalue containing a bare<followed by something that isn't a valid tag name - e.g.Test comment with a bare less-than: values <=2 break things- and one pending event for it (evePendingAlertEmail=1):Result: the process crashes with the traceback below on the very next cycle.
evePendingAlertEmailfor the test event is still1afterward (confirmed via the same query) - the crash happens before it would ever be cleared, so this recurs on every subsequent cycle unmodified.Relevant
app.confsettingsdocker-compose.yml
Debug or Trace enabled
Relevant
app.logsectionNote on that last line: as explained above,
string[:100]here is misleading - it's just the start of the whole document, not where the actual bad token is. In this run the real failure was 3375 characters in, landing exactly inside the test device'sdevCommentstext (confirmed by rebuilding the exact same HTML locally and re-runningtokenize()with real position tracking instead of yattag's own truncated message).Docker Logs
netalertx-bug3-docker-logs.txt