Skip to content

Production security & quality hardening (0.8.0) - #74

Merged
sc2ben merged 2 commits into
mainfrom
security-quality-hardening
Jul 14, 2026
Merged

sc2ben merged 2 commits into
mainfrom
security-quality-hardening

Conversation

@tsunaminoai

Copy link
Copy Markdown
Contributor

Readies zigmark for public release and untrusted-input use (PolicyPress), mirroring the pozeiden v0.3.0 hardening run. All findings were confirmed at the source and locked in with a regression suite. CommonMark/GFM conformance is preserved (652/652 + 24/24), and om ci passes (test, fuzz, fmt, wasm).

HTML — XSS

  • URL-scheme filtering, default-on. javascript:/vbscript:/file:/non-image data: destinations in links, images, and autolinks render with an empty href/src. Detection normalises the scheme like a browser (leading control/space stripping, single-byte entity decoding so javascript: is caught, tab/newline/CR stripping). A denylist, so made-up-scheme:// and data:image/{png,gif,jpeg,webp} still pass — conformance unchanged.
  • Footnote labels escaped — were injected raw into an attribute and element text.
  • Opt-in safe mode escapes raw HTML to visible text: html.Options{ .safe = true }, zigmark.renderHtmlWithOptions, the zigmark_render_html_safe C ABI export, and the CLI --safe flag. Default output is byte-identical.

Typst — code injection (compile-time file read)

  • Code spans/blocks now emit via raw() with string-literal escaping, so backticks/fences can't break out into executable Typst.
  • Frontmatter-derived preamble fields are validated before interpolation (fontsize as a length, colours as hex, strings escaped) with safe fallbacks. The unquoted fontsize was the strongest injection candidate.

DoS / panics

  • Parser.max_nesting_depth (128) and Parser.max_input_bytes (16 MiB, 0 = unlimited) turn deeply-nested / oversized input into error.NestingTooDeep / error.InputTooLarge instead of stack overflow / unbounded memory. Error paths free the partial tree (leak-checked under std.testing.allocator).
  • Fixed the ATX # u8 overflow and the frontmatter @intCast panics.

Frontmatter (#73)

  • A YAML parse failure no longer leaks the parser's error bundle, asserts on parser internals, or writes diagnostics to stderr from library code. The CLI frontmatter/markdown formats degrade gracefully instead of aborting.
  • The underlying rejection of a plain scalar containing an inline " - " is an upstream zig-yaml bug; documented as needing a fork-level fix.

Tooling

  • Fuzz harness fixed — every target used the pre-0.16 fn (void, []const u8) signature and never compiled against Zig 0.16's std.testing.fuzz; now uses *std.testing.Smith. Added targets for safe-mode HTML, Typst, and ZON; wired a zig build fuzz smoke check into CI. Fixed the fuzz--fuzz typo in the dev-shell app.
  • zig fmt --check gate added as a flake check. Deliberate hand-alignment is preserved with // zig fmt: off / on guards; incidental formatting normalised.
  • New src/markdown/security_test.zig regression suite (run by zig build test).

Docs / release

  • CHANGELOG ### Security section, README "Rendering Untrusted Markdown" section, SECURITY.md guidance + known limitations, version 0.7.4 → 0.8.0.

Known limitation

The inline link/emphasis scanner is O(n²) on pathological unmatched-bracket input — a CPU-DoS bounded (not eliminated) by max_input_bytes. A delimiter-stack rewrite is deferred to a follow-up; untrusted-input deployments should set a conservative max_input_bytes and a call-site timeout.

Prepares zigmark for public release / untrusted input (PolicyPress).

HTML XSS:
- Filter dangerous URL schemes (javascript:/vbscript:/file:/non-image data:)
  by default in links, images, and autolinks, with browser-style scheme
  normalisation; denylist keeps CommonMark/GFM conformance (652/652 + 24/24).
- Escape footnote labels (were injected raw into attribute + element text).
- Add opt-in safe mode (html.Options.safe / renderHtmlWithOptions /
  zigmark_render_html_safe / CLI --safe) that escapes raw HTML to visible text.

Typst injection (compile-time file read):
- Emit code spans/blocks via raw() with string-literal escaping.
- Validate frontmatter-derived preamble fields (length/hex/string) with safe
  fallbacks.

DoS / panics:
- Parser.max_nesting_depth (128) and max_input_bytes (16 MiB) turn pathological
  input into NestingTooDeep / InputTooLarge instead of stack overflow / OOM;
  error paths free the partial tree.
- Fix ATX '#' u8 overflow and frontmatter @intcast panics (std.math.cast).

Frontmatter (#73):
- YAML parse failure no longer leaks the error bundle, asserts, or writes to
  stderr from library code; CLI degrades gracefully. (Underlying plain-scalar
  " - " rejection is an upstream zig-yaml limitation.)

Tooling:
- Fix fuzz harness (was broken on Zig 0.16: old []const u8 signature ->
  *std.testing.Smith); add safe-HTML/Typst/ZON targets; CI fuzz smoke check.
- Add zig fmt --check gate; guard deliberate hand-alignment with
  // zig fmt: off/on.
- New src/markdown/security_test.zig regression suite.
@sc2ben sc2ben added bug Something isn't working testing Tests for common compliance labels Jul 14, 2026
@sc2ben
sc2ben self-requested a review July 14, 2026 04:16
@sc2ben
sc2ben merged commit 3bf2c1c into main Jul 14, 2026
4 checks passed
@sc2ben
sc2ben deleted the security-quality-hardening branch July 14, 2026 04:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working testing Tests for common compliance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Frontmatter YAML: inline " - " in a plain scalar throws ParseFailure (seq_item_ind)

2 participants