Skip to content

A device's free-text devComments containing an unescaped < crashes the entire notification build (yattag.indentation.XMLTokenError), silently dropping every pending alert in the same cycle #1743

Description

@mormegil6

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.

  • Firefox
  • Chrome
  • Edge
  • Safari (unsupported) - PRs welcome
  • N/A - This is an issue with the backend

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

  1. Fresh NetAlertX instance. LOG_LEVEL='trace'.
  2. 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);
  3. 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

  • I have read and followed the steps in the wiki link above and provided the required debug logs and the log section covers the time when the issue occurs.

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

Metadata

Metadata

Labels

bug 🐛Something isn't workingnext release/in dev image 🚀This is coming in the next release or was already released if the issue is Closed.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions