Skip to content

Check parser type before instantiating class in DefaultFTPFileEntryParserFactory.createFileEntryParser()#403

Merged
garydgregory merged 2 commits into
apache:masterfrom
dxbjavid:ftp-parser-factory-type-check
Jul 5, 2026
Merged

Check parser type before instantiating class in DefaultFTPFileEntryParserFactory.createFileEntryParser()#403
garydgregory merged 2 commits into
apache:masterfrom
dxbjavid:ftp-parser-factory-type-check

Conversation

@dxbjavid

@dxbjavid dxbjavid commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Untrusted class name from the SYST reply is instantiated during listing auto-detection

When FTPClient.listFiles() runs without an explicit parser key or config it auto-detects the parser from the server's SYST reply, and DefaultFTPFileEntryParserFactory treats a reply that looks like a qualified class name as a class to load. It calls Class.forName and then getConstructor().newInstance(), only catching the ClassCastException afterwards, so the constructor and static initialiser of any class on the client's classpath run before the type is ever checked. A malicious or intercepted server can reply to SYST with the name of an arbitrary classpath class and have it built for its side effects (CWE-470). The impact is limited to classes already present, but it is still an unintended instantiation driven by remote input.

The type check is moved ahead of construction with isAssignableFrom, so unrelated classes are rejected before their constructor runs. The documented behaviour of passing your own parser class name is unchanged, since real parser classes still load and construct as before.

  • Read the contribution guidelines for this project.
  • Read the ASF Generative Tooling Guidance if you use Artificial Intelligence (AI).
  • I used AI to create any part of, or all of, this pull request. Which AI tool was used to create this pull request, and to what extent did it contribute?
  • Run a successful build using the default Maven goal with mvn; that's mvn on the command line by itself.
  • Write unit tests that match behavioral changes, where the tests fail if the changes to the runtime are not applied. This may not always be possible, but it is a best practice.
  • Write a pull request description that is detailed enough to understand what the pull request does, how, and why.
  • Each commit in the pull request should have a meaningful subject line and body. Note that a maintainer may squash commits during the merge process.

During listing auto-detection the parser key is taken from the server's SYST reply. A key shaped like a qualified class name was passed to Class.forName and constructed before the type was checked, so any classpath class could be instantiated for its side effects. Verify the class implements FTPFileEntryParser before calling the constructor.

@garydgregory garydgregory left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dxbjavid
The class is still loaded and initialized, this doesn't change anything. You'll want to avoid initializing the class if you really want to bullet-proof this. How can this actually be an issue?

Class.forName(key) initialises the class, so an unrelated class named
in the key ran its static initialiser before isAssignableFrom rejected
it. Load with the three-arg forName (initialize=false) so only a real
FTPFileEntryParser is initialised, when it is constructed. Test now
asserts the static initialiser does not run.

Signed-off-by: Javid Khan <dxbjavid@gmail.com>
@dxbjavid

dxbjavid commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

you're right, good catch. the single-arg Class.forName still initialises the class, so the static initialiser ran before isAssignableFrom ever looked at the type. moving the check earlier only stopped the constructor, not the static init.

i've switched to the three-arg Class.forName(key, false, loader) so the class is loaded without being initialised. it only gets initialised when a genuine FTPFileEntryParser is actually constructed. the test now asserts the static initialiser doesn't run, and it fails against the initialising forName (expected false, was true), so it guards the real behaviour now.

on how much this actually matters: it's narrow. the class has to already be on the client's classpath and have a visible side effect in its static block, and the trigger is the SYST reply during listFiles auto-detection, so a malicious or intercepted server picks which existing class gets initialised. no arbitrary code beyond what's already shipped. more defence-in-depth than a sharp vuln, but loading without init is basically free and keeps remote input from initialising unrelated classes, so it seemed worth tightening.

@garydgregory

Copy link
Copy Markdown
Member

@dxbjavid
I'm not connecting the dots. How does this method get called with a custom class name? I see the method is called from FTPClient.createParser(String) which is called from FTPClient.initiateListParsing(String, String) which is called from:

  • org.apache.commons.net.ftp.FTPClient.initiateListParsing(String)
  • org.apache.commons.net.ftp.FTPClient.listFiles(String, FTPFileFilter)
  • org.apache.commons.net.ftp.FTPClient.listFiles(String)

All of the above call initiateListParsing with null.

@dxbjavid

dxbjavid commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

right, that null is the parserKey argument, and it's actually the null that makes this reachable. when createParser gets a null key it falls through to the autodetect branch (the else at ~1236 in FTPClient): it calls getSystemTypeOverride(), which with no FTP_SYSTEM_TYPE property set just returns getSystemType(), and that's the server's SYST reply, _replyLines.get(last).substring(4). that string is then passed straight into createFileEntryParser(systemType).

so the value that reaches the factory isn't null, it's whatever the server put in its SYST response. normal servers answer UNIX/WINDOWS/etc and hit the alias path, but if a server replies to SYST with something shaped like a qualified class name (say some.pkg.Foo) it matches JAVA_QUALIFIED_NAME_PATTERN and goes down the Class.forName branch instead. nothing constrains the SYST text to the known aliases.

full chain for listFiles(path): initiateListParsing(null, path) -> createParser(null) -> else/autodetect -> getSystemTypeOverride() -> getSystemType() -> SYST reply substring(4) -> createFileEntryParser(systemType).

as noted it's narrow (needs a class already on the client classpath with a side effect in its static block), so it's more defence-in-depth than a sharp vuln, but loading without init keeps a remote SYST reply from initialising unrelated classes.

@garydgregory garydgregory changed the title check parser type before instantiating class in createFileEntryParser Check parser type before instantiating class in createFileEntryParser Jul 5, 2026
@garydgregory garydgregory changed the title Check parser type before instantiating class in createFileEntryParser Check parser type before instantiating class in DefaultFTPFileEntryParserFactory.createFileEntryParser() Jul 5, 2026
@garydgregory
garydgregory merged commit 6e38749 into apache:master Jul 5, 2026
11 checks passed
@garydgregory

Copy link
Copy Markdown
Member

Ok, I see it now, thank you for the clarification @dxbjavid , merged 🚀

garydgregory added a commit that referenced this pull request Jul 5, 2026
DefaultFTPFileEntryParserFactory.createFileEntryParser() (#403).
@garydgregory

Copy link
Copy Markdown
Member

@dxbjavid
One issue that is Commons Net related is https://issues.apache.org/jira/browse/VFS-857 Would you have a look please?

@dxbjavid

dxbjavid commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

sure, happy to take a look. from a quick read it looks like a Net-side gap rather than pure VFS: our autodetect only flips the control encoding to UTF-8 when the server advertises it in FEAT (the hasFeature("UTF8") check in connectAction), and IIS notably doesn't list UTF8 in FEAT even though it accepts OPTS UTF8 ON. so setAutodetectUTF8 quietly does nothing there and the filenames come back mis-decoded. i'll dig into it properly on the VFS issue and see whether the right fix is on the Net side (optionally sending OPTS UTF8 ON) or in how VFS drives the client, and follow up there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants