Skip to content

misp-stix 2026.9.8 - Hardened STIX import, catchable failures, input limits & security fixes

Latest

Choose a tag to compare

@chrisr3d chrisr3d released this 08 Sep 12:26
· 44 commits to main since this release
d646d09

Changelog - 2026.7.8 → 2026.9.8

This release delivers the remediation of a full security review of the library conducted in July 2026. The hardening landed as two pull requests on the import pipeline (#105, #106), seven themed feature branches (#111–#117), and a follow-up run of fixes committed directly on dev. Both conversion directions are covered: hostile documents can no longer reach outside the conversion, several classes of silent data loss are now reported, and where and how output files are written is settled. Every fix shipped with a regression test measured failing before it; the suite now stands at 2,083 tests. The consumer-visible behaviour changes are collected in their own section near the end.

Security advisories

Four of the fixed findings are published as security advisories on the vulnerability lookup. Each affects every release up to and including 2026.7.8 and is fixed in 2026.9.8:

Advisory Severity (CVSS 4.0) Fixed by
GCVE-1-2026-20174 - path traversal in MISP object template resolution during STIX import and export 8.8 High a8b6808, a0f5407
GCVE-1-2026-20053 - denial of service via malformed or oversized STIX documents 8.7 High 6611955, e8e732a
GCVE-1-2026-20011 - parser confusion and mass assignment allow unauthorized MISP attribute metadata injection 6.9 Medium 66c654b, 3e5e7bd
GCVE-1-2026-20136 - cross-document parser state contamination when a parser instance is reused 6.3 Medium f659393, ad4f0a6, f08373d

The advisory pages carry the full details - mechanism, impact, workarounds for unpatched versions.

Hostile documents can no longer reach outside the conversion

A STIX document is third-party input, and several places trusted it with decisions input should never make:

Object template resolution. pymisp resolves a MISP object template by joining the object name into a filesystem path, so a name taken from bundle content could carry separators or .. segments and read a definition.json outside the template directory. On import, such a name is now converted as a generic object, with the name kept as data and a warning recorded; on export, names stored in an event are checked before they reach pymisp, and a name that cannot be a template name is replaced, kept in the object comment and reported.

Tag injection. A MISP tag has no escape sequence, and text a document supplied - a producer name, a galaxy name or type, a marking's fields - was written straight into taxonomy tags: a value carrying a " closed its slot and had the rest of itself read as further tags, so a bundle could tag the event it created with the tlp:, PAP: or workflow: values of its choosing. Every taxonomy tag is now built by one builder on the parser both directions inherit from: what a slot cannot carry is removed and each alteration reported, a slot nothing survives from yields no tag rather than an empty one, and a galaxy cluster whose value cannot be a tag value is tagged by its uuid so the tag still names the cluster. An invariant test, run in CI with the other suites, asserts no tag is built anywhere else - the number of injection points is now a property of the design, not of the last search for them. Clean content round-trips byte for byte. The tags a sender writes whole (tlp:*, PAP:*, workflow:*, a bundle's labels) stay verbatim: a sender's own tag is theirs to write, a slot in a tag this library authors is not.

Local file reads through the STIX 1 loader. lxml treats a plain string as a filename or a URL, so a caller passing an untrusted file:// string turned a conversion request into a local file read. String input is now resolved to a path at loader entry.

Fields a bundle must not set. Attributes of an x-misp-object accepted any field the bundle supplied, including distribution, sharing group id and tags. Only the fields the export side writes are imported now; anything else is dropped with a warning naming the keys.

The Internal/External classification. The choice between the faithful round-trip parser and the external one was made solely from labels and header titles - content any producer can write - with no way to opt out. The new classification parameter (--classification on the command line) pins the choice; detection stays the default but is advisory, and disagreements are reported as warnings.

Host paths in error text. Error messages and failure reports named the input's resolved host-side directory, in one case twice. Both directions now name the file and what went wrong, nothing more. Repaired along the way: a collection export whose inputs had all failed still built an output - the STIX 2 path raised out of the entry function, the STIX 1 paths wrote an empty package over a previous export's file.

  • a8b6808, a0f5407 - template names validated on import and export
  • ca7242d, 428b10c, 617a99c, 1a69bb2, 6bc958f - producer tag sanitised; one tag builder for every taxonomy tag, Marking Definitions included; the invariant test in CI
  • 16a06dd - loader input paths resolved; retry-on-failure narrowed to the unregistered-namespace case
  • 66c654b - Custom object attribute fields allow-listed
  • 3e5e7bd - classification / --classification override
  • ff2ebfa, 6371716 - input directories out of error texts; all-inputs-failed collections refuse instead of writing

Silent data loss is now reported (STIX 2 import)

A set of situations where a bundle lost or overrode data and the operator was never told. In each case the resolution stays what it was - the fix is that the loss is named, with the id or uuid the records actually carry (issues #107–#110 among them):

  • Two objects sharing one STIX id: the last still wins - it is the version the producer means - but a warning names the id.

  • Two STIX ids of different types collapsing onto one MISP uuid (a MISP record uuid keeps only the part after --), tracked separately for events, objects and attributes; and the same collision for galaxy clusters, whose uuid derivation never includes the object type - reported at every place a cluster uuid is assigned, the ACS marking path included.

  • Values dropped when an Indicator is folded back into its Observed Data pair: the fold stays, the values the Observed Data does not carry are named.

  • Invalid Marking Definitions sharing one id: unlike other invalid objects a marking is applied, so the survivor governed data the sender never marked - no longer in silence.

  • object_refs pointing at objects the bundle never carried: every reference type now names the id it could not reach, and a bundle with no observable objects at all answers the reference instead of raising.

  • Invalid objects nothing references: previously indistinguishable from a document that never carried them; swept and reported at the end of the parse, without double-reporting what a reference already surfaced.

  • An Event Report Note a 2.0 bundle carried in dict form vanished without an error; it converts again.

  • 6594615 - duplicate STIX ids

  • 361075d, 701052f - record and galaxy-cluster uuid collisions

  • 55c0127, 5102893, 385e056, 9568877, cf3f9dd - Indicator/Observed-Data merge, duplicated invalid markings, dangling object_refs, unreferenced invalid objects, dict-form Event Report Notes

Failures are catchable, and a result always says what happened

Loading failures used to call sys.exit: SystemExit derives from BaseException, so a caller's except Exception never saw them and one malformed document could kill a long-lived importer. They now raise the exported STIXLoadingError (parsing before loading raises MissingSTIXContentError), and the import entry functions guard the whole conversion - detection, parser construction, parsing - returning the documented error dict (fixes #88).

A conversion that dropped objects used to return a bare {'success': 1} unless the caller asked for debug. Errors and warnings now travel with every result dict; debug only selects their level of detail (fixes #104). The same applies when the conversion fails: a failure result carries everything the parser recorded before the crash, not just the crash line. A converter raising something unexpected now costs one object rather than the run, reported with the id of the object it cost. The loading helpers were also made safe to reuse - a shared mutable default kept invalid objects across calls for the process lifetime, and bare 'objects' / 'id' strings surfaced in place of messages (fixes #89, #90). The README documents the errors a successful conversion can report.

  • 6611955, 7b74f51 - catchable exceptions; reusable loading helpers
  • 7907ea6, b77b681 - unconditional error reporting, documented
  • 2fca3b3, 3d899f6 - failure results keep the recorded messages; converter escapes reported per object

Input size limits, and the cost of a conversion stated

An import parsed whatever it was handed, in full, before anything looked at it - the sender chose the cost. A STIX document above 100 MB is now refused before it is read: files are sized with stat(), in-memory content by the bytes it holds, and the refusal is STIXInputSizeError, a STIXLoadingError subclass every existing handler already catches (fixes #99). The limit is the caller's: max_size in bytes on the loaders, both parsers and the entry functions, --max-input-size in MB on the command line, 0 to remove it. A STIX 1 document whose root element is not a STIX package is refused by a cheap pull-parser peek before the tree is ever built, and the STIX 2 loader now deserialises a document once instead of twice. Content handed over as a dict or list is deliberately not sized - the caller already materialised it.

The README now states what a conversion costs - 2 to 7 times the input size in memory, seconds of a single core per MB of STIX 2 - and that a limit bounds one conversion, not the load a host accepts: an integrator running imports concurrently has to cap how many run at once.

Where output files land, and how they are written

Several default output locations resolved into the installed package tree - read-only on a normal install, and next to the library's own data files on any other; on a source checkout, collection exports had been quietly writing into the repository root, while four STIX 1 paths raised FileNotFoundError on documented defaults. Every default now writes next to the input it read, a directory the caller names is created rather than failing at write time, and the temp fragments a streamed STIX 1 collection produces live in a tempfile scratch directory removed however the assembly ends.

How the file is created changed too: every output goes through one choke point that writes to a scratch file in the destination's directory, 0o600 whatever the umask, and moves it into place in one step - an interrupted conversion leaves the previous file exactly as it was. A destination that already exists is refused with a FileExistsError unless the caller passes overwrite=True (--overwrite). output_dir now works as a str everywhere it is documented to. And stix_1_to_misp produces one MISP event - which it always did, the Internal parser merging related packages like the External one - so its default parameters no longer crash.

  • d76be8b, 0c7fe35 - defaults next to the input; scratch directory for collection fragments
  • c76fe86 - owner-only, atomic writes with an overwrite opt-in
  • 602e1c3, b62958f - output_dir as a str; one event per STIX 1 import

One conversion no longer inherits another's state

A STIX 1 parser instance reused for a second package kept everything the first one put on it - DNS bookkeeping turned into another document's passive-dns objects, titles, dates and timestamps merged across documents, galaxies and references carried over. All three STIX 1 parsers now reset per package through the same hook the STIX 2 parsers have, whose own reset was extended to the galaxy and cluster state it had been leaving populated - including the cluster a previous document defined, which could answer a reference the next document never defined (fixes #101).

On the export side, the identifier namespace mixbox keeps as a process-wide global was set after the input had been converted and never unset: every generated id carried the previous export's organisation prefix, and the value leaked to whatever the caller generated afterwards. The three STIX 1 export entry functions now run under a scoped namespace, restored on the way out whether the export succeeds or raises (fixes #102). Concurrent exports still share mixbox's one generator; that limitation is now written down in the README rather than implied.

Export correctness and STIX conformance

Pattern property names are quoted. A MISP object relation went into x_misp_<relation> inside a STIX pattern as it stood: metacharacters produced patterns the specification rejects, and a relation like a.b stayed valid while quietly asserting an object path instead of one property name. The relation is quoted now and keeps every character it had.

Custom property names conform to the specification. STIX 2.1 requires custom property names to stay within [a-z0-9_]; the sigma and suricata export sites skipped the dash-to-underscore fold every sibling site applies, so one relation yielded two spellings depending on the object carrying it. Both now normalise - the output changes: a dashed relation that used to export as x_misp_weird-relation exports as x_misp_weird_relation, from every object type that can carry it.

The -n/--namespace parameter is validated. The value went verbatim into the Package root element's xmlns: declaration, where a " declared namespaces of its own and a < produced a file no XML parser accepts while the export reported success. Only an absolute URI is accepted now, checked before any output exists.

A registry-key-value object with an unmapped relation no longer kills the 2.1 export. The relation folds into the value type as a custom property in both versions, and the previously degraded 2.0 folded path exports the real windows-registry-key SCO too.

A collection handed to the wrong STIX 1 converter is diagnosed. Both collection parsers check the items, not only the container, before any conversion starts: a wrong-layer input raises InvalidMISPInputError naming the item that does not belong - instead of a KeyError or TypeError from inside the conversion, after part of the output was already written.

Merged STIX 1 collections are well-formed again. A JSON export merging several attribute files concatenated fragments with no separator and clipped two characters off each, producing unparseable output; and merged campaigns carried namespace declarations that did not belong on them.

  • 64fe5e3, 05e12a7 - quoted pattern property names; validated namespace parameter
  • ef54a3a - normalised sigma/suricata custom property names
  • 9692e04 - registry-key-value with an unmapped relation
  • f19d2f6 - wrong-layer collections diagnosed before conversion
  • 03aa5af, d2eeaa9 - readable merged JSON collections; campaigns without stray namespace declarations

STIX 1 import: repaired, tested, and carrying its galaxies

The STIX 1 import path referenced names that do not exist - any Course of Action, related object, Incident history section or internal import aborted with AttributeError. The call sites now reach real helpers, and the direction gained its first regression suite. A TTP carrying an exploit target - the documented way a vulnerability travels in STIX 1 - converts instead of crashing.

And the galaxies a STIX 1 import derives finally land: both parsers accumulated the galaxy tags built from threat actors, contentless TTPs and courses of action, and nothing ever read the set back - every event-level galaxy was silently dropped. The set is now applied to the event as the last parsing step, in sorted order. Events imported from STIX 1 now carry galaxy tags they previously lost; the round trip is safe, since the export strips event galaxy tags once converted to their STIX 1 objects, so the import restores rather than duplicates.

  • aff93cd, 3ef5009 - undefined-name repairs; the STIX 1 import regression suite
  • 0e3f851 - TTPs carrying an exploit target
  • 27640ec - accumulated galaxy tags applied to the event

Import robustness on unusual-but-valid STIX 2 content

  • An empty bundle - a TAXII poll that returned nothing - has no objects property in 2.1 and used to raise AttributeError; it yields an empty event.

  • Object types one STIX version does not know arrive as plain dictionaries: they were read with attribute access, raised, and dragged a misleading second error out of the report referencing them. Every incoming object is read through the interface both forms share (fixes #103), dict-form dates are parsed, and label dispatch works on both forms.

  • Galaxy objects were read as if the export-side labels were always present and well formed; absent, valueless and multi-= labels are tolerated, and an object with no usable galaxy type is reported instead of vanishing.

  • A Location or malware instance without a name - which STIX 2.1 permits - was dropped with a traceback; the cluster and tag values fall back onto the object id.

  • A galaxy type nothing a taxonomy tag can be made of took the whole event down; the record survives without the tag.

  • 95271cf, 3ee775a, 0981258 - objectless bundles; dict-form objects and their label dispatch

  • 851850f, 85cd0f5, 5dbd13b - tolerant galaxy labels; nameless galaxy objects; cluster tags that cannot be written

Connecting to MISP: the API key off the command line

A key passed with --api-key lands on argv, readable from the process list and shell history. The new MISP_URL and MISP_API_KEY environment variables fill in the flags the operator left out - precedence is flag over environment over the --config file, and empty variables count as unset - so the key never has to appear on the command line. The --help text names the variables and steers operators away from the flag.

Behaviour changes to check before upgrading

  • STIX 1 imports apply the galaxy tags they silently dropped - events converted from STIX 1 gain the event-level galaxy tags derived from threat actors, TTPs and courses of action (27640ec).
  • STIX 2 export output changes for dashed relations on sigma/suricata objects: custom property names are normalised to the specification's character set, so x_misp_weird-relation becomes x_misp_weird_relation (ef54a3a).
  • Importing misp_stix_converter no longer disables urllib3 warnings process-wide. Applications embedding the library see InsecureRequestWarning (and every other urllib3 warning) again for their own connections. The CLI still quiets InsecureRequestWarning - but only while talking to a MISP instance with certificate verification deliberately disabled (--skip-ssl, or verify_cert: false in the config file), and the warning filters are restored afterwards (b5acef4).
  • Writing onto a path that already holds a file needs overwrite=True (--overwrite); new output files are 0o600; defaults land next to the input rather than in the package tree or the working directory.
  • A document above 100 MB is refused unless the caller raises the limit (max_size, --max-input-size) or turns it off with 0.
  • Loading failures raise STIXLoadingError instead of exiting the process; a wrong-layer STIX 1 collection raises InvalidMISPInputError instead of a KeyError/TypeError from inside the conversion; load_stix2_content given a list raises STIXLoadingError instead of an uncaught TypeError.
  • The STIX 1 export entry functions restore the identifier namespace they found; a caller that relied on the leftover global must set it itself (the public framing helpers still leave it set, on purpose). A parser instance reused across packages yields one event per package instead of accumulating.
  • Result dicts always carry the recorded errors and warnings; debug only selects their verbosity.

Supply chain and dependency posture

The review's dependency and supply-chain pass ran ahead of this release; its posture, stated so consumers can weigh it themselves: stix-edh - part of the STIX 1 stack - has never published a source distribution on PyPI and its recorded upstream is gone, so the artifact cannot be tied to a public source tree; the wheel itself was inspected directly and is clean (pure Python, no XML parsing of its own, no execution primitives), and the lock pins it by sha256 hash - as it does every artifact it holds - so what installs from the lock is byte-identical to what was reviewed. The wider STIX 1 stack (stix, mixbox, cybox, maec) has been unmaintained since 2020 but is pure Python throughout: the only C code touching untrusted bytes is lxml, which is current and actively maintained. No upstream fix will ever come for the four abandoned packages, so a future vulnerability in them means vendoring or forking - an accepted posture for as long as STIX 1 support remains, and now a documented one. An OSV scan of the complete locked stack found zero known vulnerabilities in the runtime dependencies.

The setuptools runtime dependency (required for the STIX 1 stack's pkg_resources use) is now floored at >=83 - arbitrarily old releases are no longer accepted. (The floor is upper-unbounded by design: setuptools majors increment monthly, a cap would churn every release.)

The development-only pytest constraint moved past the one advisory the scan did find (dev-group only, never installed with misp-stix), and the suite runs green under pytest 9.

  • 2005593, ac12dcc - setuptools floor, pytest 9, lock refresh; a documentation-ordering nondeterminism the verification surfaced

Maintenance

The test workflow now also runs on pull requests into dev, and the mapping documentation was regenerated for the changes above - including the object template names an import cannot resolve, which are now documented.

Contributors

Credits

Thanks to Jeroen Pinoy (@Wachizungu) for the thorough security review this release delivers the remediation of.