//nefariousplan

CVE-2025-6218: WinRAR's file_name_check Produced the Path Traversal It Was Supposed to Catch

pattern

cve

proof of concept

CVE-2025-6218 is a path traversal in RARLAB's WinRAR. The function that processes archive filenames during extraction is named file_name_check. It does two jobs in one loop. It preserves .. sequences at the start of path components, by design, because the developer expected another layer downstream to anchor them. It also strips trailing dots and spaces from each component, by deletion, because Windows treats file. and file as identical to file on its own filesystem. When the function deleted a space sitting between .. and a path separator, the rest of the filename slid one position left and the .. ended up touching the separator. The path traversal was not in the input. file_name_check produced it.

The function the bug lives in does two jobs

RARLAB's WinRAR ships closed-source. The file_name_check analysed below was reverse-engineered from the 7.11 binary by an outside researcher and published with the public CVE-2025-6218 proof of concept at ignis-sec/CVE-2025-6218. The pseudocode in this post is cleaned up for readability; the structural choices that matter for the bug are faithful to the disassembly.

file_name_check is called during archive extraction. Its job is to walk each entry's stored filename and canonicalise it before the extraction subsystem opens a file handle. The function has two responsibilities baked in.

First, preserve .. sequences that appear at the start of a path component. The developer who wrote the loop wrote it expecting another layer to anchor any .. against the user's chosen extraction directory. The loop explicitly does not strip .. at component starts. The relevant branches in the disassembly are the goto LOOP_START paths inside the dot-handling code, every one of which is a "keep this character" decision.

Second, strip trailing dots and spaces from each component. Windows treats file. and file and file as the same on-disk file, a quirk inherited from FAT16 and preserved by NTFS for compatibility. An archive that contains two entries legitimate_file. and legitimate_file races to write the same disk slot, and the canonicaliser is supposed to collapse those two forms into one filename before extraction reaches the filesystem.

The strip is implemented by deletion. Each unsafe trailing character is removed with delete_char(cur_filename, cur_pos, 1u), which shifts the rest of the filename one position to the left.

void file_name_check(char *a1) {
  // outer loop: walk forward until next separator
  while (1) {
    if (str[offset + 1] == '\\' || str[offset + 1] == '/')
      break;
    ++cur_pos; ++offset;
  }

  // inner loop: process the component backward
  while (1) {
    if (str[offset] != '.' && str[offset] != ' ')
      goto LOOP_START;

    // ".." at the start of a path component is kept, by design
    if (str[offset] == '.') {
      if (!cur_pos) goto LOOP_START;
      if (str[offset - 1] == '\\' || str[offset - 1] == '/')
        goto LOOP_START;
      if (str[offset - 1] == '.' && cur_pos == 1)
        goto LOOP_START;
      if (str[offset - 1] == '.' &&
          (str[offset - 2] == '\\' || str[offset - 2] == '/'))
        goto LOOP_START;
    }

    delete_char(cur_filename, cur_pos, 1u);
    --offset;
    if (--cur_pos < 0) goto LOOP_START;
  }
}

Now trace a component shaped .. . (dot, dot, space, dot) followed by a separator. Four characters, then a / or \. The outer loop walks forward and breaks at the separator. The inner loop steps backward through the component:

  1. Iteration one, offset 3, .. The previous character (str[2]) is a space, not a separator and not a dot. None of the keep-conditions fire. delete_char removes the dot at position 3. The string shifts: the component is now .. followed by the separator. --offset, --cur_pos.
  2. Iteration two, offset 2, . The space-to-underscore branch only fires when cur_pos == 0 (start of component). It does not fire here. None of the keep-conditions fire. delete_char removes the space at position 2. The string shifts again: the component is now .. followed by the separator. --offset, --cur_pos.
  3. Iteration three, offset 1, .. The previous character is . and cur_pos == 1. The .. at-start-of-component keep rule fires. goto LOOP_START.

The component entered the function as .. .. It exits the function as ... The path separator that came after the component is now adjacent to two dots. The next thing the extraction subsystem will read from the canonicalised filename is a directive to climb out of the user's chosen extraction directory.

What the function checked, and what it produced

A path-traversal check that runs against file_name_check's input sees .. . followed by a separator. That is not a forbidden form. The dot-space-dot sequence is a legal filename byte-for-byte on FAT and NTFS; Windows resolves it differently from .. because the trailing space-and-dot is part of the name as stored. A check against the function's output sees .. followed by the same separator. That is the canonical path-traversal form.

WinRAR's extraction subsystem consumes the output. There is no second pass over the filename between file_name_check returning and the extractor opening a file handle. The check the developer presumably believed was running on the function's output ran on the function's input, or did not run at all. The keep-conditions inside the loop, which preserve .. at component starts, presuppose a downstream gate. That gate, if it exists, sees what file_name_check left behind, which is the manufactured .. rather than the original .. ..

This is the Gate Before Canonicalize shape compressed onto a single stack frame. The pattern's usual presentation is two parsers in two layers: Spring's uriDecode feeding Jetty's path resolver, Alfresco's regex denylist feeding Tomcat's ..;/ handling, s3-proxy's auth middleware reading one path while its handler reads another. In each of those, the gap between two parsers is what the attacker hides in. In WinRAR's case the gap is between file_name_check's input view and its output view, with no parser and no layer in between. The same function holds the gate logic, performs the canonicalisation, and never reads its own output.

The patch is one operator

WinRAR 7.12 shipped on 25 June 2025. The reverse-engineered post-patch loop replaces the deletion with an in-place substitution.

void file_name_check(char **a1) {
  offset = 0;
  do {
    if (str[offset + 1] == '\\' || str[offset + 1] == '/') {
      if (str[offset] == '.' || str[offset] == ' ') {
        // same preservation checks for ".." at component starts
        str[offset] = '_';   // replace, do not delete
      }
    }
    ++offset;
  } while (offset < length);
}

The same input, .. . followed by a separator, traces through 7.12 differently. The outer loop reaches position 3 (the trailing dot) and observes that position 4 is a separator. The preservation checks fire and the branch falls through to str[offset] = '_'. Position 3 becomes _. The space at position 2 is never visited, because the 7.12 loop only enters its mutation branch when the next character is a separator. The space is not the last character of its component; it stays.

The output is .. _ followed by a separator. The leftward slide that produced .. under 7.11 cannot happen, because no character is removed. The intent of the original loop, canonicalise away Windows's silent-trailing-junk filenames, is preserved. The mechanism is changed from one that shifts adjacent characters into contact to one that does not.

The patch's vector is one operator. delete_char(cur_filename, cur_pos, 1u) becomes str[offset] = '_'. Same goal, different implementation. The bug class is the difference between the two.

Opposed state actors converged on one parser

CVE-2025-6218 was published on 21 June 2025. CISA added it to the Known Exploited Vulnerabilities catalogue later in 2025. Between those two events, multiple state-aligned threat groups were observed shipping it in spear-phishing campaigns delivered as .rar attachments.

Foresiet and SecPod independently documented Bitter (APT-C-08), attributed to India, targeting Pakistani government and military entities with archives named Provision_of_Information_for_Sectoral_for_AJK.rar and similar (AJK is Azad Jammu and Kashmir, disputed territory at the Indo-Pakistani line of control). The Bitter chain uses the WinRAR primitive to drop a Word STARTUP template, triggers a PowerShell downloader on the next Word launch, and lands WmRAT, MiyaRAT, or ZxxZ on the host.

UAC-0010 / Gamaredon, attributed to Russia's FSB and targeting Ukraine, appears in a public IOC tracker maintained by the researcher palianytsia_200. The tracker catalogues the group's archive-delivered loader chain and lists CVE-2025-6218 among the delivery primitives. The scanner that surfaced this writeup picked up the tracker as a "PoC URL" and walked it back to the CVE. The tracker is not a proof of concept; it is an IOC catalogue published a year after the underlying bug was assigned. The CVE is real regardless.

The two groups have nothing in common politically. India's adversary is not Russia's adversary. Their targets do not overlap. They converged because the parser they both rely on for first-stage delivery happens to share the same flaw in the same year, and the reverse-engineered analysis was published within ten days of the patch.

Archive path traversal as a class predates this CVE by twenty-one years. The first documented instance is ..\ in WinZip 8 in 2004. The Snyk-coined name ZIP slip entered the security literature in 2018. WinRAR has been patched against archive path traversal at least four times since 2010. The class persists because every implementation eventually faces the same constraint: an archive can legitimately encode characters and sequences that resemble path traversal (volume-set parent references, trailing-dot Windows-compat filenames, Unicode lookalikes), and the implementation has to discriminate "legitimate" from "escape." Each time the discrimination is split across functions, or between sanitise and check, or between the input view and the output view of the same function, the gap re-appears.

WinRAR 7.11 split it between the input view and the output view of file_name_check.

PoC: ignis-sec/CVE-2025-6218

The function's name was file_name_check. The function did not check its own output.