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:
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
Class: CWE-522 (insufficiently protected credentials) / CWE-200 / CWE-601 (open redirect).
Severity: High — CVSS 3.1 base 6.5 (
To the WinSCP maintainers
This report is sent under coordinated disclosure. The primary issue (W1/W1b) is a
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. Primary vulnerability — W1 / W1b
Summary
When a WebDAV response is a 3xx redirect, the WinSCP client opens a **new** session to
the server-supplied
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
additionally strips TLS, so Basic credentials are sent in cleartext.
This occurs on two paths:
The enabling chain is
challenges. The source already notes the gap:
the destination host.
Attack flow
Impact
Root cause is shared by a 4-site family
The same "trust the server-supplied redirect/endpoint host" defect appears at:
(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`).
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:
- Day 0 — this report sent.
- Day 0–7 — vendor acknowledges receipt.
- Day 7–30 — vendor confirms, assigns CVE, prepares fix (patch provided).
- ≤ 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
OPTIONShandshake: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 reliably—WebDAVFileSystem.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`).