ndaal Security Advisory: Path traversal in CSAF export via attacker-controlled tracking ID (CWE-22) — fixed in csaf-crud 0.3.3

Edit JSON

Metadata

Tracking IDndaal-sa-2026-047
Categorycsaf_security_advisory
CSAF Version2.1
Statusfinal
Version1.0.0
Publisherndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG
TLPCLEAR
Initial Release2026-05-20T00:00:00.000Z
Current Release2026-05-20T00:00:00.000Z

Vulnerabilities (1)

CVETitleCVSS v3CVSS v4
-Path traversal in CSAF export via attacker-controlled tracking ID0.00.0

Raw JSON

{
  "$schema": "https://docs.oasis-open.org/csaf/csaf/v2.1/schema/csaf.json",
  "document": {
    "category": "csaf_security_advisory",
    "csaf_version": "2.1",
    "distribution": {
      "tlp": {
        "label": "CLEAR"
      }
    },
    "lang": "en",
    "notes": [
      {
        "category": "summary",
        "text": "ndaal_public_csaf_crud (the `csaf-crud` web application and the `csaf-core` library it embeds) is affected by a path-traversal vulnerability (CWE-22) in the CSAF export path. The exported file location is derived from the imported document's `document.tracking.id` field, which is fully attacker-controllable: a CSAF document accepted on import carries a tracking ID that the exporter splits into a `{year}/{sequence}` directory pair and joins directly onto the configured export directory. Document validation only checks that the tracking ID is non-empty, and the `parse_tracking_id` helper only checks that the year is four numeric digits and that the sequence contains no extra `-` separator. It does NOT reject path separators or `..` segments in the sequence. A crafted tracking ID such as `ndaal-sa-2026-../../escaped` therefore survives both checks and resolves to a directory OUTSIDE the export root, allowing an operator who imports an attacker-supplied advisory and then exports it to write CSAF JSON (and its hash sidecars) to an arbitrary filesystem location reachable by the service account. Fixed in 0.3.3 by routing every externally-derived path through a new `csaf_core::path_security::safe_join` capability-style guard that rejects `..`, absolute paths, drive/UNC prefixes, and NUL bytes before any filesystem operation runs."
      },
      {
        "category": "description",
        "text": "Affected code path (pre-fix), `crates/csaf-core/src/export.rs::export_document`:\n\n    let (year, seq) = parse_tracking_id(tracking_id, &settings.naming_convention)?;\n    ...\n    let export_dir = Path::new(&settings.export_directory);\n    let doc_dir = export_dir.join(&year).join(&seq);   // <-- unconfined join\n    std::fs::create_dir_all(&doc_dir)?;\n    let filename = format!(\"{tracking_id}.json\");\n    let file_path = doc_dir.join(&filename);            // <-- unconfined join\n    std::fs::write(&tmp_path, &json_bytes)?;\n    std::fs::rename(&tmp_path, &file_path)?;\n\nThe two `join` calls trust `year`, `seq`, and `tracking_id`, all of which are derived from `document.tracking.id` in the imported CSAF document.\n\nValidation gaps that let the malicious value through:\n  1. `crates/csaf-core/src/validation.rs::validate_tracking` only rejects a tracking ID that is empty after trimming; it does not constrain its character set or shape.\n  2. `crates/csaf-core/src/export.rs::parse_tracking_id` strips the configured prefix, splits once on `-` into (year, sequence), rejects a sequence that itself contains `-`, and requires the year to be exactly four digits parsable as a `u16`. A sequence value of `../../escaped` contains no `-`, so it passes; the year `2026` is valid; the function returns `(\"2026\", \"../../escaped\")`.\n\nResulting behaviour for `document.tracking.id = \"ndaal-sa-2026-../../escaped\"` with `export_directory = <base>/export`:\n  export_dir.join(\"2026\").join(\"../../escaped\")  ==  <base>/escaped\nand `create_dir_all` plus the atomic write create `<base>/escaped/...`, one level above the intended export root. Deeper `..` sequences reach arbitrary absolute locations writable by the service account.\n\nFix (0.3.3): a new lexical guard module `crates/csaf-core/src/path_security.rs` provides `safe_join(base, input) -> Option<PathBuf>`. It walks the path components of `input` and:\n  - returns `None` if `input` contains a NUL byte;\n  - returns `None` for any `RootDir` or `Prefix` component (absolute paths, Windows drive / UNC prefixes);\n  - pops the working stack on each `..` and returns `None` if a `..` would pop above `base`;\n  - otherwise rebuilds the path under `base`, guaranteeing the result starts with `base` and contains no `..` component.\n\n`export_document` now assembles both the `{year}/{seq}` directory and the `{tracking_id}.json` filename through `safe_join` and refuses the export (returns an error) when either resolves outside the export directory, BEFORE any directory is created or any byte is written. The companion `is_safe_relative_path` helper additionally replaces the naive `file_path.contains(\"..\")` substring check in `crates/csaf-crud/src/static_files.rs::serve_static` with a component-based check that also rejects absolute paths, drive/UNC prefixes, and NUL bytes."
      },
      {
        "category": "details",
        "text": "Changed files (fix shipped in 0.3.3):\n\n  crates/csaf-core/src/path_security.rs            new — `safe_join` + `is_safe_relative_path` lexical CWE-22 guard\n  crates/csaf-core/src/lib.rs                      register `pub mod path_security;`\n  crates/csaf-core/src/export.rs                   route `{year}/{seq}` and `{tracking_id}.json` through `safe_join`; refuse on escape\n  crates/csaf-crud/src/static_files.rs             replace `contains(\"..\")` with component-based `is_safe_relative_path`\n  crates/csaf-core/tests/test_path_security.rs     new — 57-vector path-traversal regression suite\n\nRegression coverage: the new suite drives `safe_join` against 57 hand-picked vectors plus property-style coverage, tagged by origin so a future regression points at the original threat corpus:\n  cs_*       cap-std upstream boundary-escape vectors (absolute, multi-level `..`, descend-then-escape, NUL truncation, trailing `..`).\n  owasp_*    OWASP Path Traversal cheat sheet (CWE-22): encoded `../`, double-encoded, NUL truncation (CVE-2007-0883 class), Windows / UNC / drive-letter shapes.\n  csaf_*     attacker-controllable CSAF feeder fields (`tracking_id`, advisory path, Atom `<id>` URI fragments), including a 32-level deep traversal.\n  unicode_*  normalisation lookalikes (NFC/NFD, RTL override, fullwidth full stop, zero-width joiner, division-slash homoglyph) that have produced sandbox escapes elsewhere.\nProperty-style coverage exercises every traversal depth 1..=64 (rejected), balanced pop-then-descend (rejected), balanced pop without trailing `..` (accepted), and NUL insertion at every byte position of a benign filename (rejected). An export-level regression test (`test_export_document_rejects_traversal_tracking_id`) confirms `export_document` refuses a tracking ID that escapes the export directory and that nothing is written outside it.\n\nThreat coverage:\n  CWE-22  — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')\n  CWE-23  — Relative Path Traversal\n  CWE-73  — External Control of File Name or Path\n\nVerification (0.3.3):\n  cargo test -p csaf-core --test test_path_security        57 passed; 0 failed\n  cargo test -p csaf-core -p csaf-crud --all-features      all passed\n  cargo clippy --workspace --all-targets --all-features -- -D warnings   clean\n  rust-doctor .                                            99 / 100 (Security 100)"
      },
      {
        "category": "general",
        "text": "Recommended action: upgrade to ndaal_public_csaf_crud 0.3.3 or later. No configuration change is required; the fix is enforced unconditionally in the export and static-file code paths.\n\nInterim mitigation for operators who cannot upgrade immediately:\n  - Only import CSAF documents from trusted sources, and review the `document.tracking.id` of any third-party advisory before importing or re-exporting it.\n  - Run the service under a dedicated least-privilege system user whose write access is confined to the configured export / dump / log directories, so a traversal cannot reach sensitive locations.\n\nThis advisory documents the application's own code; consumers of CSAF advisories published by ndaal are not affected by this issue."
      },
      {
        "category": "other",
        "text": "Disposition: security advisory — vendor fix available in 0.3.3. Severity assessed as MEDIUM. Although the impact (arbitrary file write as the service account) is high, exploitation requires an operator to import an attacker-supplied CSAF document and then perform an export, so the issue is operator-mediated rather than remotely self-triggering. Status: final."
      },
      {
        "category": "legal_disclaimer",
        "text": "THIS DOCUMENT IS PROVIDED ON AN 'AS IS' BASIS AND DOES NOT IMPLY ANY KIND OF GUARANTEE OR WARRANTY. ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS INFORMATION INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS."
      }
    ],
    "publisher": {
      "category": "vendor",
      "contact_details": "security@ndaal.eu",
      "issuing_authority": "ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG Security Team",
      "name": "ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG",
      "namespace": "https://ndaal.eu/csaf"
    },
    "references": [
      {
        "category": "self",
        "summary": "This advisory in CSAF 2.1 format",
        "url": "https://gitlab.com/vPierre/ndaal_public_csaf_crud/-/raw/main/csaf/2026/047/ndaal-sa-2026-047.json"
      },
      {
        "category": "external",
        "summary": "CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')",
        "url": "https://cwe.mitre.org/data/definitions/22.html"
      },
      {
        "category": "external",
        "summary": "CWE-23 — Relative Path Traversal",
        "url": "https://cwe.mitre.org/data/definitions/23.html"
      },
      {
        "category": "external",
        "summary": "CWE-73 — External Control of File Name or Path",
        "url": "https://cwe.mitre.org/data/definitions/73.html"
      },
      {
        "category": "external",
        "summary": "OWASP — Path Traversal",
        "url": "https://owasp.org/www-community/attacks/Path_Traversal"
      },
      {
        "category": "external",
        "summary": "Bytecode Alliance cap-std — capability-based filesystem APIs (design basis for the safe_join guard)",
        "url": "https://github.com/bytecodealliance/cap-std"
      },
      {
        "category": "external",
        "summary": "ndaal_public_csaf_crud repository",
        "url": "https://gitlab.com/vPierre/ndaal_public_csaf_crud"
      }
    ],
    "title": "ndaal Security Advisory: Path traversal in CSAF export via attacker-controlled tracking ID (CWE-22) — fixed in csaf-crud 0.3.3",
    "tracking": {
      "current_release_date": "2026-05-20T00:00:00.000Z",
      "generator": {
        "engine": {
          "name": "ndaal CSAF Generator",
          "version": "1.0.0"
        }
      },
      "id": "ndaal-sa-2026-047",
      "initial_release_date": "2026-05-20T00:00:00.000Z",
      "revision_history": [
        {
          "date": "2026-05-20T00:00:00.000Z",
          "number": "1.0.0",
          "summary": "Initial security advisory documenting a path-traversal vulnerability (CWE-22) in the CSAF export path of ndaal_public_csaf_crud. The export location is derived from the imported document's `document.tracking.id`; validation and `parse_tracking_id` did not reject `..` or path separators in the sequence component, so a crafted tracking ID could escape the configured export directory and write files to an arbitrary location. Fixed in 0.3.3 by routing all externally-derived paths through the new `csaf_core::path_security::safe_join` guard (and replacing the static-file `contains(\"..\")` check with a component-based one), backed by a 57-vector regression suite. Status: final."
        }
      ],
      "status": "final",
      "version": "1.0.0"
    }
  },
  "product_tree": {
    "branches": [
      {
        "category": "vendor",
        "name": "ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG",
        "branches": [
          {
            "category": "product_name",
            "name": "csaf-crud",
            "branches": [
              {
                "category": "product_version_range",
                "name": "vers:cargo/<0.3.3",
                "product": {
                  "name": "ndaal_public_csaf_crud (csaf-crud / csaf-core) before 0.3.3 — CSAF export joins the imported document's tracking-ID-derived `{year}/{seq}` and `{tracking_id}.json` directly onto the export directory without confinement, allowing a crafted `document.tracking.id` containing `..` to escape the export root (path traversal / arbitrary file write).",
                  "product_id": "CSAFPID-0001"
                }
              },
              {
                "category": "product_version",
                "name": "0.3.3",
                "product": {
                  "name": "ndaal_public_csaf_crud (csaf-crud / csaf-core) 0.3.3 — every externally-derived path is routed through `csaf_core::path_security::safe_join`, which rejects `..`, absolute paths, drive/UNC prefixes, and NUL bytes before any filesystem operation; static-file serving uses the component-based `is_safe_relative_path` check.",
                  "product_id": "CSAFPID-0002"
                }
              }
            ]
          }
        ]
      }
    ]
  },
  "vulnerabilities": [
    {
      "notes": [
        {
          "category": "description",
          "text": "The CSAF exporter derives the on-disk location of an exported advisory from the imported document's `document.tracking.id`. `export_document` splits the tracking ID into a `{year}/{sequence}` pair via `parse_tracking_id` and joins both — plus the `{tracking_id}.json` filename — directly onto the configured export directory. Document validation only rejects an empty tracking ID, and `parse_tracking_id` only enforces a four-digit year and the absence of an extra `-` in the sequence. A tracking ID such as `ndaal-sa-2026-../../escaped` passes both checks and resolves to a directory above the export root, so `create_dir_all` and the atomic write place files outside the intended directory. With deeper `..` sequences the write reaches arbitrary locations writable by the service account.",
          "title": "Description"
        },
        {
          "category": "details",
          "text": "Mitigation (0.3.3) — two layers. (1) Lexical: the new `csaf_core::path_security::safe_join` / `is_safe_relative_path` guard confines every externally-derived path to its base directory, rejecting `..` escapes, absolute paths, drive/UNC prefixes, and NUL bytes; `export_document` validates the assembled directory and filename and aborts before any filesystem mutation if either would escape the export directory, and the static-file handler's naive `contains(\"..\")` check is replaced with the component-based `is_safe_relative_path`. (2) Capability: all advisory import, export, dump, and audit-log filesystem access is routed through capability-scoped `cap-std` directory handles (`csaf_core::fs::DataDir`), so `..`, absolute paths, and symlink/TOCTOU escapes are refused at the syscall layer (Linux `openat2(RESOLVE_BENEATH)`) — defence in depth beneath the lexical check. A 57-vector regression suite, an export-level test, and a `fuzz_safe_join` cargo-fuzz target lock in the behaviour.",
          "title": "Mitigation"
        }
      ],
      "product_status": {
        "known_affected": [
          "CSAFPID-0001"
        ],
        "fixed": [
          "CSAFPID-0002"
        ]
      },
      "remediations": [
        {
          "category": "none_available",
          "details": "No fix is available for affected versions (< 0.3.3); the vulnerable export path is present in every release before 0.3.3. Upgrade to 0.3.3 (CSAFPID-0002) to obtain the fix.",
          "product_ids": [
            "CSAFPID-0001"
          ]
        },
        {
          "category": "vendor_fix",
          "details": "Upgrade to ndaal_public_csaf_crud 0.3.3 (CSAFPID-0002), which routes all externally-derived paths through `csaf_core::path_security::safe_join` and refuses any export whose tracking ID would escape the export directory.",
          "product_ids": [
            "CSAFPID-0002"
          ]
        }
      ],
      "metrics": [
        {
          "content": {
            "cvss_v3": {
              "version": "3.1",
              "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N",
              "baseScore": 0.0,
              "baseSeverity": "NONE",
              "attackVector": "NETWORK",
              "attackComplexity": "LOW",
              "privilegesRequired": "NONE",
              "userInteraction": "NONE",
              "scope": "UNCHANGED",
              "confidentialityImpact": "NONE",
              "integrityImpact": "NONE",
              "availabilityImpact": "NONE"
            },
            "cvss_v4": {
              "version": "4.0",
              "vectorString": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N",
              "baseScore": 0.0,
              "baseSeverity": "NONE",
              "attackVector": "NETWORK",
              "attackComplexity": "LOW",
              "attackRequirements": "NONE",
              "privilegesRequired": "NONE",
              "userInteraction": "NONE",
              "vulnConfidentialityImpact": "NONE",
              "vulnIntegrityImpact": "NONE",
              "vulnAvailabilityImpact": "NONE",
              "subConfidentialityImpact": "NONE",
              "subIntegrityImpact": "NONE",
              "subAvailabilityImpact": "NONE"
            }
          },
          "products": [
            "CSAFPID-0001"
          ]
        }
      ],
      "title": "Path traversal in CSAF export via attacker-controlled tracking ID",
      "references": [
        {
          "category": "external",
          "summary": "CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')",
          "url": "https://cwe.mitre.org/data/definitions/22.html"
        },
        {
          "category": "external",
          "summary": "OWASP — Path Traversal",
          "url": "https://owasp.org/www-community/attacks/Path_Traversal"
        }
      ]
    }
  ]
}

Are you sure?