Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Nightly Fuzzing & Sanitizers

on:
schedule:
# Run every night at 02:00 UTC
- cron: '0 2 * * *'
workflow_dispatch:
inputs:
duration_seconds:
description: 'Duration to fuzz in seconds'
required: false
default: '600'
type: string

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
fuzz:
name: Nightly ASan / UBSan libFuzzer Campaign
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7

- name: Setup Rust nightly toolchain
uses: dtolnay/rust-toolchain@nightly

- name: Install cargo-fuzz
run: cargo install cargo-fuzz

- name: Seed initial corpus
run: |
mkdir -p fuzz/corpus/fuzz_parser
cp fuzz/seeds/*.xml fuzz/corpus/fuzz_parser/ || true

- name: Run libFuzzer with AddressSanitizer and XML dictionary
run: |
DURATION="${{ github.event.inputs.duration_seconds || '1800' }}"
echo "Starting fuzzing campaign for ${DURATION}s..."
cargo +nightly fuzz run fuzz_parser -- \
-dict=fuzz/xml.dict \
-max_total_time="${DURATION}" \
-print_final_stats=1

- name: Archive crash artifacts if found
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzz-crash-artifacts
path: |
fuzz/artifacts/
crash-*
leak-*
timeout-*
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,17 @@ For benchmark data and attack reproduction suites, see the **[Industrial Validat

---

## ⚡ Continuous Structure-Aware Fuzzing & Sanitizers

SafeXML incorporates continuous fuzz testing to guarantee memory safety and zero panics across arbitrary inputs and parameter configurations:

- **Structure-Aware libFuzzer**: Randomized `ParseOptions` fuzz target (`fuzz/fuzz_targets/fuzz_parser.rs`) exploring permutations of depth limits, attribute bounds, and DTD permissions.
- **XML Syntax Dictionary**: Seeded with `fuzz/xml.dict` containing XML grammar tokens, DTD specifiers, namespace declarations, and known attack strings.
- **Nightly Sanitizer Pipeline**: Automated GitHub Actions workflow (`.github/workflows/fuzz.yml`) running libFuzzer under **AddressSanitizer (ASan)** and **UndefinedBehaviorSanitizer (UBSan)** with crash artifact archiving.
- **Google OSS-Fuzz**: Integration manifests prepared in `oss-fuzz/` for 24/7 continuous distributed cluster fuzzing.

---

## 🏗️ Architectural Distinction: `safexml` vs `polyxml`

Both projects are maintained by the **[PolyXML](https://github.com/polyxml)** organization:
Expand Down
3 changes: 3 additions & 0 deletions fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
target/
corpus/
artifacts/
54 changes: 52 additions & 2 deletions fuzz/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"
arbitrary = { version = "1.4", features = ["derive"] }

[dependencies.safexml]
path = ".."
Expand Down
60 changes: 50 additions & 10 deletions fuzz/fuzz_targets/fuzz_parser.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,57 @@
#![no_main]

use _safexml::parser::{parse_xml_bytes, ParseOptions};
use arbitrary::{Arbitrary, Unstructured};
use libfuzzer_sys::fuzz_target;

#[derive(Debug, Arbitrary)]
struct FuzzInput<'a> {
forbid_dtd: bool,
forbid_entities: bool,
forbid_external: bool,
max_depth: u16,
max_entity_size: u32,
max_text_size: u32,
max_attributes: u16,
max_attribute_size: u32,
max_name_size: u16,
max_comment_size: u32,
max_input_size: u32,
max_elements: u32,
xml_data: &'a [u8],
}

fuzz_target!(|data: &[u8]| {
let options = ParseOptions {
forbid_dtd: true,
forbid_entities: true,
forbid_external: true,
max_depth: 64,
max_elements: 10_000,
max_input_size: 1_000_000,
..Default::default()
};
let _ = parse_xml_bytes(data, &options);
// If the data is large enough to unpack structured parameters, randomize options.
// Otherwise, treat the entire byte slice as XML data with default/strictest options.
if let Ok(input) = FuzzInput::arbitrary(&mut Unstructured::new(data)) {
let options = ParseOptions {
forbid_dtd: input.forbid_dtd,
forbid_entities: input.forbid_entities,
forbid_external: input.forbid_external,
// Bound limits to reasonable fuzzing ranges so we test limit enforcement
// without allocating gigabytes of memory per iteration.
max_depth: (input.max_depth as usize % 512).max(1),
max_entity_size: (input.max_entity_size as usize % (1024 * 1024)).max(1),
max_text_size: (input.max_text_size as usize % (1024 * 1024)).max(1),
max_attributes: (input.max_attributes as usize % 1024).max(1),
max_attribute_size: (input.max_attribute_size as usize % (1024 * 1024)).max(1),
max_name_size: (input.max_name_size as usize % 2048).max(1),
max_comment_size: (input.max_comment_size as usize % (1024 * 1024)).max(1),
max_input_size: (input.max_input_size as usize % (2 * 1024 * 1024)).max(1),
max_elements: (input.max_elements as usize % 100_000).max(1),
};
let _ = parse_xml_bytes(input.xml_data, &options);
} else {
let options = ParseOptions {
forbid_dtd: true,
forbid_entities: true,
forbid_external: true,
max_depth: 64,
max_elements: 10_000,
max_input_size: 1_000_000,
..Default::default()
};
let _ = parse_xml_bytes(data, &options);
}
});
1 change: 1 addition & 0 deletions fuzz/seeds/basic.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<root attr="test">Hello World</root>
5 changes: 5 additions & 0 deletions fuzz/seeds/dtd_entity.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY test "payload">
]>
<root>&test;</root>
1 change: 1 addition & 0 deletions fuzz/seeds/multiple_roots.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<root><child/></root><extra/>
5 changes: 5 additions & 0 deletions fuzz/seeds/namespaces_cdata.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<root xmlns="http://example.com/ns" xmlns:a="http://example.com/a">
<a:child id="1"><![CDATA[raw & unescaped text]]></a:child>
<!-- comment token -->
<empty/>
</root>
106 changes: 106 additions & 0 deletions fuzz/xml.dict
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# AFL / libFuzzer XML dictionary
# Syntax delimiters
"<"
">"
"</"
"/>"
"<?xml"
"?>"
"<!--"
"-->"
"<![CDATA["
"]]>"
"<!DOCTYPE"
"<!ENTITY"
"<!ELEMENT"
"<!ATTLIST"
"<!NOTATION"

# XML declaration keywords & attributes
"version="
"\"1.0\""
"\"1.1\""
"encoding="
"\"UTF-8\""
"\"UTF-16\""
"\"ISO-8859-1\""
"\"US-ASCII\""
"standalone="
"\"yes\""
"\"no\""

# DTD keywords & entity specifiers
"SYSTEM"
"PUBLIC"
"NDATA"
"EMPTY"
"ANY"
"PCDATA"
"#PCDATA"
"CDATA"
"ID"
"IDREF"
"IDREFS"
"ENTITY"
"ENTITIES"
"NMTOKEN"
"NMTOKENS"
"#REQUIRED"
"#IMPLIED"
"#FIXED"

# Standard character & predefined entities
"&lt;"
"&gt;"
"&amp;"
"&apos;"
"&quot;"
"&#"
"&#x"
"&#60;"
"&#62;"
"&#38;"
"&#39;"
"&#34;"
"&#x3C;"
"&#x3E;"
"&#x26;"
"&#x27;"
"&#x22;"

# XML Namespaces
"xmlns"
"xmlns:"
"xmlns:a="
"xmlns:b="
"xmlns:xml="
"\"http://www.w3.org/XML/1998/namespace\""
"\"http://www.w3.org/2000/xmlns/\""

# Common attack tokens & bomb payloads
"file:///"
"http://"
"https://"
"ftp://"
"expect://"
"php://"
"data:"
"&lol;"
"&lol1;"
"&lol2;"
"&lol3;"
"&lol4;"
"&lol5;"
"&lol6;"
"&lol7;"
"&lol8;"
"&lol9;"
"&xxe;"
"%xxe;"
"&all;"
"%all;"

# Processing instruction names
"<?target"
"<?xml-stylesheet"

5 changes: 5 additions & 0 deletions oss-fuzz/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM gcr.io/oss-fuzz-base/base-builder-rust:v1

RUN git clone --depth 1 https://github.com/polyxml/safexml.git safexml
WORKDIR safexml
COPY build.sh $SRC/
Loading
Loading