Skip to content

Improve FactoryFinder Validation#1793

Merged
cshannon merged 3 commits intoapache:mainfrom
cshannon:factory-finder-validation
Mar 18, 2026
Merged

Improve FactoryFinder Validation#1793
cshannon merged 3 commits intoapache:mainfrom
cshannon:factory-finder-validation

Conversation

@cshannon
Copy link
Contributor

Also modernizes with generics and adds improved testing

@cshannon cshannon requested a review from jbonofre March 17, 2026 22:45
Also modernizes with generics and adds improved testing
@cshannon cshannon force-pushed the factory-finder-validation branch from 4adfd2f to 2fe1f53 Compare March 17, 2026 22:52
jbonofre
jbonofre previously approved these changes Mar 18, 2026
public Object create(String path) throws IllegalAccessException, InstantiationException, IOException, ClassNotFoundException {
public Object create(String path)
throws IllegalAccessException, InstantiationException, IOException, ClassNotFoundException {
throw new UnsupportedOperationException("Create is not supported without requiredType and allowed impls");
Copy link
Member

Choose a reason for hiding this comment

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

nit: as it's a "breaking change", we should remember to note in the release note.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would not call this a breaking change because ActiveMQ never calls this method. FactoryFinder now calls the new method in the ObjectFactory interface so this new method is never invoked unless someone extended or implemented the interface themselves or wrote something custom. If someone has their own version of ObjectFactory everything works fine because the new method just delegates to the legacy. This is only breaking if it's expected users would invoke Activator themselves and not just the broker but I don't see why we would expect that.

Copy link
Contributor

@jeanouii jeanouii left a comment

Choose a reason for hiding this comment

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

Great rework @cshannon

We can maybe discuss about the backward compatible part if we have the fix on 6.3.0 maybe. If we don't have the backward compatibility we can't backport to previous versions. Trade off. I'm fine either way.

It looks great.
Only thing to verify is the Windows compatibility. The rest is more discussion/comment for sharing purposes.

throw new InstantiationException("Provided key escapes the FactoryFinder configured directory");
}

return resolvedPath.toString();
Copy link
Contributor

Choose a reason for hiding this comment

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

With your comment above, this might fail on Windows, because /dir1/dir2/file will probably be in reality \dir1\dir2\file so I think you will probably need to consider doing
return resolvedPath.toString().replace('\', '/');

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think we need to do this because forward slashes should work fine with windows in java as this validation check doesn't result in any different output than previously. Previously we just used the path with forward slashes and add on the key, now we do the same but before returning just resolve it and check it first. So the resulting value should be the same as before on both windows/linux if it passes validation.

Class clazz = classMap.get(path);
public Object create(final String path)
throws IllegalAccessException, InstantiationException, IOException, ClassNotFoundException {
throw new UnsupportedOperationException("Create is not supported without requiredType and allowed impls");
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as @jbonofre did above for the Activator.

@SuppressWarnings("unchecked")
default <T> T create(String path, Class<T> requiredType, Set<Class<? extends T>> allowedImpls)
throws IllegalAccessException, InstantiationException, IOException, ClassNotFoundException {
return (T) create(path);
Copy link
Contributor

Choose a reason for hiding this comment

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

This is bypassing all the class validation, path validation and allowed impls. Anyone setting a custom ObjectFactory can bypass validations.
Is it a design choice?
Maybe a javadoc comment if that's the case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was done by default for backwards compatibility and also I don't know why we would do anything if someone plugs in their own impl. If someone provides their own custom impl then it is on them to do whatever they want in my opinion.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a javadoc that makes mention it's up to the implementations to decide if the parameters are used. Maybe in 6.3.0 or another version where we can have breaking changes we could just drop the create(path) method entirely and just have the new method. Ultimately though, as I said, it's just an interface so implementations can ignore or do what they want.


@SuppressWarnings("unchecked")
private static <T> Set<Class<? extends T>> loadAllowedImpls(Class<T> requiredType, String allowedImpls) {
// If allowedImpls is either null or an asterisk (allow all wild card) then set to null so we don't filter
Copy link
Contributor

Choose a reason for hiding this comment

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

Useful comment to move to javadoc maybe so it can benefit developers/contributors.

Now, I have one concern. The default 2 args contructor defaults to allow-all. Usually from a security perspective, we tend to deny-all by default. This PR being about improving security, I'm surprised. That being said, I see that for internet facing factories (Stomp and Web) are using a strict list of impl which is great.
For the internal factories (like TransportFactory), the default is applied and we use allow all whereas we know in advance the list, so we could switch to deny all by default and provide the list of known implementations for internal factories as well.
What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's not really much point of having a 2 arg constructor if the default is deny all as it's pointless to create a FactoryFinder that can't do load anything. We could just only have the 3 arg constructor and then just update all the uses to provide the known lists

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually there are a ton of impls for some of them and the impls are not all loaded on the same classpath so it's not easy to build the list. I think we can just get rid of the 2 arg and have the 3 arg and at least make the calling code explicilty set null if not caring to check

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with this compromise.

// Normalize the base path with the given key. This
// will resolve/remove any relative ".." sections of the path.
// Example: "/dir1/dir2/dir3/../file" becomes "/dir1/dir2/file"
final Path resolvedPath = Path.of(path).resolve(key).normalize();
Copy link
Contributor

Choose a reason for hiding this comment

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

need to move toAbsolutePath somehow (both paths) to be comparable otherwise the startsWith can fail even if "true"

.map(Class::getName).collect(Collectors.joining(","));
}

public static <T> void validateClass(Class<?> clazz, Class<T> requiredType,
Copy link
Contributor

Choose a reason for hiding this comment

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

if you want to validate the class you must validate the class name before loading it otherwise you can have side effects and defeat the validation no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The class is is fine to load, what you want to do is avoid initialization and calling classloader.loadClass() does not initialize

Copy link
Contributor

Choose a reason for hiding this comment

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

You actually can't assume that with AMQ since it runs in complex classloader envs where it is wrong several (in some OSGi or EE setup for ex)

}
}
if (clazz == null) {
clazz = FactoryFinder.class.getClassLoader().loadClass(className);
Copy link
Contributor

Choose a reason for hiding this comment

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

classloader can be null there so a fallback on the system classloader is likely needed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was existing behavior so could be changed in a future PR if needed

@cshannon cshannon merged commit c9d3963 into apache:main Mar 18, 2026
10 checks passed
jbonofre pushed a commit to jbonofre/activemq that referenced this pull request Mar 18, 2026
Also modernizes with generics and adds improved testing
jbonofre pushed a commit to jbonofre/activemq that referenced this pull request Mar 18, 2026
Also modernizes with generics and adds improved testing
jbonofre added a commit that referenced this pull request Mar 18, 2026
Also modernizes with generics and adds improved testing

Co-authored-by: Christopher L. Shannon <cshannon@apache.org>
jbonofre added a commit that referenced this pull request Mar 18, 2026
Also modernizes with generics and adds improved testing

Co-authored-by: Christopher L. Shannon <cshannon@apache.org>
jbonofre added a commit to jbonofre/activemq that referenced this pull request Mar 18, 2026
jbonofre added a commit to jbonofre/activemq that referenced this pull request Mar 18, 2026
jbonofre added a commit that referenced this pull request Mar 18, 2026
…1801)

* Backport: remove parallel annotation on ProtocolConverterTest (#1793)

* Cleanup import
jbonofre added a commit to jbonofre/activemq that referenced this pull request Mar 18, 2026
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.

4 participants