Post a reply

Before posting, please read how to report bug or request support effectively.

Bug reports without an attached log file are usually useless.

Options
Add an Attachment

If you do not want to add an Attachment to your Post, please leave the Fields blank.

(maximum 10 MB; please compress large files; only common media, archive, text and programming file formats are allowed)

Options

Topic review

Yukihiro Nakamura

Coordinated Vulnerability Disclosure — WinSCP WebDAV credential leak on cross-origin redirect (and b

Coordinated Vulnerability Disclosure — WinSCP WebDAV credential leak on cross-origin redirect (and bundled client-side findings)

Advisory ID: WINSCP-WEBDAV-REDIRECT-2026-001 (reporter-assigned; CVE requested)
Primary vulnerability: W1 / W1b — WebDAV client sends user credentials to an
attacker-controlled host on a cross-origin / cross-scheme redirect.
Affected: WinSCP 6.6.1.beta (source-reviewed); likely all releases sharing the
TWebDAVFileSystem redirect logic — vendor to confirm the introduced/affected range.
Class: CWE-522 (insufficiently protected credentials) / CWE-200 / CWE-601 (open redirect).
Severity: High — CVSS 3.1 base 6.5 (AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N).


To the WinSCP maintainers

This report is sent under coordinated disclosure. The primary issue (W1/W1b) is a
default-configuration credential leak to an attacker-chosen host. A working PoC and
a candidate patch are included. Bundled below are several lower-severity client-side
findings discovered in the same review, so they can be triaged together.

We request acknowledgement and propose a 90-day coordinated-disclosure window. We are
happy to adjust the timeline, validate fixes, and withhold public details until a release
is available.

Reporter: Yukihiro Nakamura
Contact: nakamurabeer@gmail.com
Report date: 2026-06-07
Proposed public-disclosure date: report date + 90 days (negotiable)
CVE: requested (reporter will request via _«CNA»_ unless vendor prefers to assign)

Suggested timeline:

  1. Day 0 — this report sent.
  2. Day 0–7 — vendor acknowledges receipt.
  3. Day 7–30 — vendor confirms, assigns CVE, prepares fix (patch provided).
  4. ≤ Day 90 — fixed release published; coordinated public disclosure.


1. Primary vulnerability — W1 / W1b

Summary
When a WebDAV response is a 3xx redirect, the WinSCP client opens a **new** session to
the server-supplied Location URL and re-arms it with the user's username + decrypted
password, without checking that the redirect target is the same host and scheme as
the configured session. A malicious or compromised WebDAV server — or a network MITM able
to inject a redirect — thereby harvests the victim's credentials. A redirect to http://
additionally strips TLS, so Basic credentials are sent in cleartext.

This occurs on two paths:

  • W1 — file download (GET): WebDAVFileSystem.cpp:1855-1868
    (// Contrary to other actions, for "GET" we support any redirect).
  • W1b — the connect-time OPTIONS handshake: WebDAVFileSystem.cpp:257-296
    (NeonClientOpenSessionInternal). W1b leaks on session *open*, before any download.

The enabling chain is NeonOpen (:324) → NeonAddAuthentication (:385) →
NeonRequestAuth (:1969), which supplies the credentials to whatever host now
challenges. The source already notes the gap:

// We should test if the redirect is not for another server, though not sure how to do this reliablyWebDAVFileSystem.cpp:830

CheckRedirectLoop (NeonIntf.cpp:221) bounds the hop count (5) but does not restrict
the destination host.

Attack flow
victim WinSCP ── connect / download ──►  origin (malicious or MITM'd)

victim WinSCP ◄──── 302 Location: http://attacker/x ──── origin
victim WinSCP ── request http://attacker/x ───────────► ATTACKER
victim WinSCP ◄──── 401 WWW-Authenticate: Basic ──────── ATTACKER
victim WinSCP ── Authorization: Basic base64(user:pass) ──► ATTACKER   ← credential leak


Impact

  • Credential theft (WebDAV username + password) to an attacker-chosen host, in the
    default configuration, with no user action beyond a normal connect/download.
  • Cleartext if the redirect downgrades to http://. Basic = plaintext password;
    Digest = offline-crackable challenge. NTLM/Negotiate are SPN-bound (less exposed).
  • W1b widens the window: simply opening a session to a hostile/MITM'd server suffices.


Root cause is shared by a 4-site family
The same "trust the server-supplied redirect/endpoint host" defect appears at:
| Site | File:line | Secret exposed | Note |

|-------|--------------------------------|----------------|------|
| W1    | `WebDAVFileSystem.cpp:1857`    | Basic/Digest password | raw `IsRedirect` |
| W1b   | `WebDAVFileSystem.cpp:269/289` | Basic/Digest/NTLM/Negotiate | connect-time |
| S3-2  | `S3FileSystem.cpp:1116`        | STS token + host-bound SigV4 | TLS-verified; secret key not sent |
| THttp | `Http.cpp:115-120` | IMDSv2 token | only reachable via a malicious metadata endpoint |


(The WebDAV PROPFIND/MOVE/COPY redirects via `IsValidRedirect` are **safe** — they discard
the redirect host and stay on the original session.)

### CVSS 3.1
- **Base 6.5** — `AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N`.
- Rationale: network attacker (malicious server), no privileges, victim initiates the
connection/download (`UI:R`), full credential disclosure (`C:H`). `AC:L` because the
malicious-server variant needs no special conditions (the MITM-only variant would be `AC:H`).
- **Alternative 7.4** with `S:C` if the leaked credential is scoped to a different
security authority than the WinSCP client.

### Proof of concept
`exploits/webdav_credstealer.py` — a stdlib-only malicious WebDAV origin that 302-redirects
to a different origin which issues a Basic 401 and decodes the resent `Authorization`.

```
$ python3 webdav_credstealer.py --selftest
[ W1 (download / GET) WebDAVFileSystem.cpp:1857 ]
>>> LEAKED to attacker -> victim_user:S3cr3t-WinSCP-Passw0rd
[ W1b (connect / OPTIONS) WebDAVFileSystem.cpp:257-296 ]
>>> LEAKED to attacker -> victim_user:S3cr3t-WinSCP-Passw0rd
```

Real-client cross-check (demonstrates this is a genuine client-trust bug): a
standards-compliant client refuses, a client that trusts cross-host redirects (== WinSCP's
behavior) leaks:
```
curl -u victim_user:S3cr3t -L http://origin/loot # DEFAULT -> no leak
curl -u victim_user:S3cr3t --location-trusted http://origin/loot # == WinSCP -> credentials captured
```

### Suggested fix (patch included)
Gate both redirect-following sites on a same-origin check before re-arming credentials,
and refuse `https→http` downgrade while credentials are attached.
Patch: `patches/0001-W1-W1b-webdav-redirect-same-origin.patch` — adds
`TWebDAVFileSystem::IsSameOriginRedirect()` (scheme + host + port match) and applies it at
`:1857` (W1) and in the connect loop (W1b). The same gate is recommended for the S3
`<Endpoint>` adoption and the `THttp` redirect. (Dry-run/apply/reverse validated against
6.6.1.beta; compile-test in the C++Builder toolchain before release.)

---

## 2. Bundled findings (same review — please triage together)

| ID | Title | CVSS 3.1 | File:line | PoC | Patch |
|----|-------|----------|-----------|-----|-------|
| **C-F2** | `DecryptFileName` 16-byte heap over-read (server-triggered DoS; needs "encrypt file names") | **3.1** (→5.3 A:H) | `Cryptography.cpp:911` | `cf2_decryptfilename_oob.cc` (ASAN) | `0002-…` |
| **F1** | SCP `\` path traversal → arbitrary local write (needs "Replace invalid chars" off) | **5.3** | `ScpFileSystem.cpp:2570` | `scp_evil_responder.py` | `0003-…` |
| **F2** | SCP unbounded `D`-record recursion → stack-exhaustion DoS | **4.3** | `ScpFileSystem.cpp:2643` | `scp_dos_recursion.py` | `0003-…` |
| **F3** | SCP server-supplied negative size → abort DoS | **4.3** | `ScpFileSystem.cpp:2566` | `scp_dos_negsize.py` | `0003-…` |
| S3-1 | S3 response-logging unbounded accumulation (DoS, logging on) | 4.3 | `S3FileSystem.cpp:709` | — | — |
| S3-2 | S3 `<Endpoint>`/region adopted from error XML (W1 family) | 3.1 | `S3FileSystem.cpp:1116` | — | (same as 0001) |
| SS-4 | SSH banner/prompt shown unsanitized (UI/log spoof) | 3.1 | `PuttyIntf.cpp:329` | — | — |
| SS-1 | Host-key dialog fail-open default (`default: Verified=true`); no reachable bypass shown | n/a | `SecureShell.cpp:2763` | — | recommend `=false` |
| C-F1 | `AES256Verify` 16-byte over-read (local, crafted store) | 2.5 | `Cryptography.cpp:341` | — | — |
| P1a | Proxy command from a hostile session file → local code exec (trust property) | 7.8 | `SecureShell.cpp:263` | — | warn/confirm |

> C-F2 is notable as the **only server-facing memory-safety defect** found, but its impact
> is a non-deterministic crash under a non-default feature, hence the low CVSS.

---

## 3. Verified NOT vulnerable (to save triage effort)

- **Parser memory safety:** libFuzzer + ASAN on `SetListingStr` (852k execs) and the SFTP
`TSFTPPacket`/`GetFile` packet parser (1.87M execs) — **no** memory-safety violation.
- **SSH host-key verification:** sound (exact compare; no bypass).
- **Bundled libraries:** OpenSSL 3.5.5 / expat 2.7.5 / neon 0.37.1 / PuTTY 0.83 / libs3 4.1
— current; Terrapin (CVE-2023-48795) and PuTTY P-521 (CVE-2024-31497) already fixed in 0.83.
- **PuTTY-integration glue:** no memory/format-string/injection bug; server bytes are
bounds-safe `UnicodeString`.

---

## 4. Disclosure terms

For authorized security research and coordinated disclosure only. No third-party systems
were accessed; all PoCs run against local test harnesses. We will not publish technical
details before the agreed disclosure date or a fixed release, whichever is first, and will
credit per the vendor's preference. Please reply to the contact above to acknowledge and to
adjust the timeline if needed.

**Artifacts available on request:** PoCs (`exploits/`), patches (`patches/`), fuzz harnesses
(`fuzz/`), and full technical write-ups (`findings-report*.md`, `variants.md`, `sca-summary.md`).