You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@freemarker.apache.org by Christoph Rüger <c....@synesty.com> on 2019/10/30 10:40:39 UTC

Freemarker: Model Wrapping in OSGI with different Classloaders

Hi Daniel,
based on our recent conversation about the security issue, we have
continued adding some things and I wanted to bring this up. Maybe something
is worth considering to add it to Freemarker...


*OSGI and Model Wrapping*
In an OSGI Application each bundle has a different classloader. In our case
BundleA can objects to the Freemarker Context and theoretically an object
of ClassA could become accessible (through a chain of getters when using
BeanModel). Our ObjectWrapper resides in BundleB and cannot see ClassA of
BundleA. Our CustomObjectWrapper lives in BundleB.

That's why we looked for a way how BundleA can register a custom
WrappingCallback which is used by our CustomObjectWrapper in BundleB.

Here is what we came up with:

BundleA -> calls FMRegistry.registerCustomObjectWrapper(new
CustomFreemarkerJsonWrapper())

BundleB.CustomObjectWrapper --> calls FMRegistry.wrap(object)

BundleB FMRegistry looks like this:


/**

* Registers a custom Object Wrapper for Freemarker

* object wrapping.

* This can be used by other bundles to provide

* custom wrappers for classes which are not known or visible here.

*

* *@param* clazz

* *@param* customObjectWrapper

*/

*public* static *void*
registerCustomObjectWrapper(CustomFreemarkerObjectWrapperCallback
customObjectWrapperCallback) {

List<Class> supportedTypes = customObjectWrapperCallback
.getSupportedTypes(); // e.g. Arrays.asList(JsonPrimitive.class,
JsonArray.class);

*for* (Class clazz : supportedTypes) {

*if*(!*this*.customFreemarkerObjectWrappers.containsKey(clazz)) {

*this*.customFreemarkerObjectWrappers.put(clazz, customObjectWrapperCallback
);

}

}

}



/**

* Used by {@link MyObjectWrapper} to wrap

* unknown types which were registered by {@link
SynestyTemplateMethodModelWithCustomObjectWrapper}

*

* *@param* obj

* *@return* a callback if found, or <code>null</code> if no callback was
registered for the given class (by obj.getClass())

*/

*public* CustomFreemarkerObjectWrapperCallback wrap(Object obj) {

*if*(obj == *null*) *return* *null*;

*return* *this*.customFreemarkerObjectWrappers.get(obj.getClass());

}


Then in our CustomObjectWrapper in BundleB we do something like this
.wrap(obj) or .handleUnknownType(obj)

// check if a customObjectWrapper is registered

        BiFunction<Object, BeansWrapper, Object> customWrapper =
FMRegistry.wrap(object);

        *if*(customWrapper != *null*) {



        Object wrappedObject = customWrapper.apply(object, *this*);

        *if*(wrappedObject != *null*) {

        *return* wrap(wrappedObject);

        }

        }

/* ... */
return super.wrap(object) // or


Here is an example of such a CallbackWrapper in BundleA:

/**

 * A custom ObjectWrapper Callback for GSON Json Objects. Callback because
it

 * will be called by our {@link MyObjectWrapper} during wrapping phase on
the

 * fly.

 *

 * e.g. to be able to convert gson-JsonArray into a ArrayList (instead of

 * Iterable) so that we can access it in Freemarker as a sequence e.g.
seq[0],

 * seq[1] etc.

 *

 */

*public* *class* CustomFreemarkerJsonWrapper *implements*
CustomFreemarkerObjectWrapperCallback {


/**

* Returns the classes which this wrapper supports.

*

* *@return*

*/

@Override

*public* List<Class> getSupportedTypes() {

*return* Arrays.*asList*(JsonPrimitive.*class*, JsonArray.*class*);

}


@Override

*public* Object apply(Object t, BeansWrapper beansWrapper) {

*if* (t *instanceof* JsonArray) {


JsonArray asJsonArray = (JsonArray) t;

*return* *new* JsonArrayFreemarkerWrapper(asJsonArray, beansWrapper);

} *else* *if* (t *instanceof* JsonPrimitive) {

*return* *new* JsonPrimitiveFreemarkerWrapper((JsonPrimitive) t,
beansWrapper);

} *else* {

*return* t;

}

}


}

*To conclude:*
FMRegistry allows several bundles to register a custom objectWrapping
method for different classes.
Our custom ObjectWrapper can use those callbacks to wrap the objects which
are unknown to its own Classloader.

This allows us to control ObjectWrapping in different bundles and solve
some classloader issues and avoid direct dependencies of bundles.

This is also needed to solve our security issue, as you suggested to always
build own WrapperModels for "dangerous" objects.

 I will write another mail regarding more the security aspect, which is
related but would be too much for this part.


Let me know if you think the above makes sense. I can also bring it up in
the mailing list, but first wanted to check with you directly.

Thanks
Christoph




-- 
Christoph Rüger, Geschäftsführer
Synesty <https://synesty.com/> - Anbinden und Automatisieren ohne
Programmieren

-- 
Synesty GmbH
Moritz-von-Rohr-Str. 1a
07745 Jena
Tel.: +49 3641 
5596493Internet: https://synesty.com <https://synesty.com>
Informationen 
zum Datenschutz: https://synesty.com/datenschutz 
<https://synesty.com/datenschutz>

Geschäftsführer: Christoph Rüger

Unternehmenssitz: Jena
Handelsregister B beim Amtsgericht: Jena

Handelsregister-Nummer: HRB 508766
Ust-IdNr.: DE287564982

Re: Freemarker: Model Wrapping in OSGI with different Classloaders

Posted by Christoph Rüger <c....@synesty.com>.
Am Do., 31. Okt. 2019 um 00:17 Uhr schrieb Daniel Dekany <
daniel.dekany@gmail.com>:

> Hi Christoph,
>
> Ideally, you register your custom wrappers to a singleton ObjectWrapper
> instance, which you get from the also singleton
> freemarker.template.Configuration instance. I'm not sure if you already
> have some shared container object (like a Spring context) that exposes
> singletons. If not, you can share the singleton Configuration through a
> static field though. My point is that this is the duty of the ObjectWrapper
> instance, not of a global static registry. Well, the static registry surely
> works in your application.

Ok. Our static registry was mainly for demonstration purposes to show the
idea.


> But, the idea of registering custom
> "sub-wrappers" under DefaultObjectWrapper came up in the past, although not
> to work around OSGi complications, but so that people can extend
> DefaultObjectWrapper (or some other stock subclass of it -
> ExtendableDefaultObjectWrapper or such) without actually extending the
> class.

 Then it's possible to "extend" the stock ObjectWrapper on runtime.

That could also help with this OSGi issue.
>
That sounds good. I mean OSGI was a bit exotic in the past, but maybe with
Java9 Module system the concepts of strong encapsulation become more
main-stream in the future. But in OSGI the "at runtime" is a key part,
because new bundles (basically new java code) can appear at runtime without
JVM-restart (used in Plugin Systems... e.g. Atlassian Confluence / JIRA use
OSGI too for their plugins as far as I know).


> One issue I spotted in above (possibly simplified) code is that you just
> look up the concrete class in the Map of the registry. Subclasses can break
> that lookup. It's more likely that you want to look up the class, and if
> that fails then the superclass of it, and so on. And then there are also
> the implemented interfaces that should be looked up.
>
Yes you are right, that is a very simplistic first draft.


> As of the need for custom classes like  JsonPrimitiveFreemarkerWrapper, I
> think that's only necessary if you need some special functionality, like
> exposing a JsonObject as a hash (consider XML DOM wrapper in FreeMarker as
> an example).

Otherwise white-listing should be able to take care of the

security aspect. I guess at least. That though will still require the
> bundle to add its own white-list rules to the ObjectWrapper, but that's not
> a "sub-wrapper", just some list of methods. Actually, I'm not sure what
> JsonPrimitiveFreemarkerWrapper does. Is it a TemplateModel?
>
Yes, we use it to wrap a GSON-Objects (in this case JsonPrimitive) to
customize the getAsString() output (remove unnecessary double quotes which
are added by .toString()). In our case we often create customized
TemplateModels just to simplify ?string output. We also have a
JsonArrayFreemarkerWrapper
(for GSON-JsonArray) so that a JsonArray is treated as an FM-CollectionModel
and TemplateScalarModel.


> Note that my goal is that commonly needed functionality goes into
> FreeMarker, and being able to add wrapping rules dynamically (i.e. without
> subclassing the stock ObjectWrapper) is a such thing. White-listing based
> security is too, though that's a different topic.
>
Ok, great. I mean if anything of the above is worth being added, great. I
just wanted to bring it up for discussion.

BTW, you did write to the mailing list already (if you check the address
> you will see). No problem, just saying.
>
Oh, thanks, I clicked send too fast :)


> On Wed, Oct 30, 2019 at 11:40 AM Christoph Rüger <c....@synesty.com>
> wrote:
>
> > Hi Daniel,
> > based on our recent conversation about the security issue, we have
> > continued adding some things and I wanted to bring this up. Maybe
> something
> > is worth considering to add it to Freemarker...
> >
> >
> > *OSGI and Model Wrapping*
> > In an OSGI Application each bundle has a different classloader. In our
> case
> > BundleA can objects to the Freemarker Context and theoretically an object
> > of ClassA could become accessible (through a chain of getters when using
> > BeanModel). Our ObjectWrapper resides in BundleB and cannot see ClassA of
> > BundleA. Our CustomObjectWrapper lives in BundleB.
> >
> > That's why we looked for a way how BundleA can register a custom
> > WrappingCallback which is used by our CustomObjectWrapper in BundleB.
> >
> > Here is what we came up with:
> >
> > BundleA -> calls FMRegistry.registerCustomObjectWrapper(new
> > CustomFreemarkerJsonWrapper())
> >
> > BundleB.CustomObjectWrapper --> calls FMRegistry.wrap(object)
> >
> > BundleB FMRegistry looks like this:
> >
> >
> > /**
> >
> > * Registers a custom Object Wrapper for Freemarker
> >
> > * object wrapping.
> >
> > * This can be used by other bundles to provide
> >
> > * custom wrappers for classes which are not known or visible here.
> >
> > *
> >
> > * *@param* clazz
> >
> > * *@param* customObjectWrapper
> >
> > */
> >
> > *public* static *void*
> > registerCustomObjectWrapper(CustomFreemarkerObjectWrapperCallback
> > customObjectWrapperCallback) {
> >
> > List<Class> supportedTypes = customObjectWrapperCallback
> > .getSupportedTypes(); // e.g. Arrays.asList(JsonPrimitive.class,
> > JsonArray.class);
> >
> > *for* (Class clazz : supportedTypes) {
> >
> > *if*(!*this*.customFreemarkerObjectWrappers.containsKey(clazz)) {
> >
> > *this*.customFreemarkerObjectWrappers.put(clazz,
> > customObjectWrapperCallback
> > );
> >
> > }
> >
> > }
> >
> > }
> >
> >
> >
> > /**
> >
> > * Used by {@link MyObjectWrapper} to wrap
> >
> > * unknown types which were registered by {@link
> > SynestyTemplateMethodModelWithCustomObjectWrapper}
> >
> > *
> >
> > * *@param* obj
> >
> > * *@return* a callback if found, or <code>null</code> if no callback was
> > registered for the given class (by obj.getClass())
> >
> > */
> >
> > *public* CustomFreemarkerObjectWrapperCallback wrap(Object obj) {
> >
> > *if*(obj == *null*) *return* *null*;
> >
> > *return* *this*.customFreemarkerObjectWrappers.get(obj.getClass());
> >
> > }
> >
> >
> > Then in our CustomObjectWrapper in BundleB we do something like this
> > .wrap(obj) or .handleUnknownType(obj)
> >
> > // check if a customObjectWrapper is registered
> >
> >         BiFunction<Object, BeansWrapper, Object> customWrapper =
> > FMRegistry.wrap(object);
> >
> >         *if*(customWrapper != *null*) {
> >
> >
> >
> >         Object wrappedObject = customWrapper.apply(object, *this*);
> >
> >         *if*(wrappedObject != *null*) {
> >
> >         *return* wrap(wrappedObject);
> >
> >         }
> >
> >         }
> >
> > /* ... */
> > return super.wrap(object) // or
> >
> >
> > Here is an example of such a CallbackWrapper in BundleA:
> >
> > /**
> >
> >  * A custom ObjectWrapper Callback for GSON Json Objects. Callback
> because
> > it
> >
> >  * will be called by our {@link MyObjectWrapper} during wrapping phase on
> > the
> >
> >  * fly.
> >
> >  *
> >
> >  * e.g. to be able to convert gson-JsonArray into a ArrayList (instead of
> >
> >  * Iterable) so that we can access it in Freemarker as a sequence e.g.
> > seq[0],
> >
> >  * seq[1] etc.
> >
> >  *
> >
> >  */
> >
> > *public* *class* CustomFreemarkerJsonWrapper *implements*
> > CustomFreemarkerObjectWrapperCallback {
> >
> >
> > /**
> >
> > * Returns the classes which this wrapper supports.
> >
> > *
> >
> > * *@return*
> >
> > */
> >
> > @Override
> >
> > *public* List<Class> getSupportedTypes() {
> >
> > *return* Arrays.*asList*(JsonPrimitive.*class*, JsonArray.*class*);
> >
> > }
> >
> >
> > @Override
> >
> > *public* Object apply(Object t, BeansWrapper beansWrapper) {
> >
> > *if* (t *instanceof* JsonArray) {
> >
> >
> > JsonArray asJsonArray = (JsonArray) t;
> >
> > *return* *new* JsonArrayFreemarkerWrapper(asJsonArray, beansWrapper);
> >
> > } *else* *if* (t *instanceof* JsonPrimitive) {
> >
> > *return* *new* JsonPrimitiveFreemarkerWrapper((JsonPrimitive) t,
> > beansWrapper);
> >
> > } *else* {
> >
> > *return* t;
> >
> > }
> >
> > }
> >
> >
> > }
> >
> > *To conclude:*
> > FMRegistry allows several bundles to register a custom objectWrapping
> > method for different classes.
> > Our custom ObjectWrapper can use those callbacks to wrap the objects
> which
> > are unknown to its own Classloader.
> >
> > This allows us to control ObjectWrapping in different bundles and solve
> > some classloader issues and avoid direct dependencies of bundles.
> >
> > This is also needed to solve our security issue, as you suggested to
> always
> > build own WrapperModels for "dangerous" objects.
> >
> >  I will write another mail regarding more the security aspect, which is
> > related but would be too much for this part.
> >
> >
> > Let me know if you think the above makes sense. I can also bring it up in
> > the mailing list, but first wanted to check with you directly.
> >
> > Thanks
> > Christoph
> >
> >
> >
> >
> > --
> > Christoph Rüger, Geschäftsführer
> > Synesty <https://synesty.com/> - Anbinden und Automatisieren ohne
> > Programmieren
> >
> > --
> > Synesty GmbH
> > Moritz-von-Rohr-Str. 1a
> > 07745 Jena
> > Tel.: +49 3641
> > 5596493Internet: https://synesty.com <https://synesty.com>
> > Informationen
> > zum Datenschutz: https://synesty.com/datenschutz
> > <https://synesty.com/datenschutz>
> >
> > Geschäftsführer: Christoph Rüger
> >
> > Unternehmenssitz: Jena
> > Handelsregister B beim Amtsgericht: Jena
> >
> > Handelsregister-Nummer: HRB 508766
> > Ust-IdNr.: DE287564982
> >
>
>
> --
> Best regards,
> Daniel Dekany
>

-- 
Synesty GmbH
Moritz-von-Rohr-Str. 1a
07745 Jena
Tel.: +49 3641 
5596493Internet: https://synesty.com <https://synesty.com>
Informationen 
zum Datenschutz: https://synesty.com/datenschutz 
<https://synesty.com/datenschutz>

Geschäftsführer: Christoph Rüger

Unternehmenssitz: Jena
Handelsregister B beim Amtsgericht: Jena

Handelsregister-Nummer: HRB 508766
Ust-IdNr.: DE287564982

Re: Freemarker: Model Wrapping in OSGI with different Classloaders

Posted by Daniel Dekany <da...@gmail.com>.
Hi Christoph,

Ideally, you register your custom wrappers to a singleton ObjectWrapper
instance, which you get from the also singleton
freemarker.template.Configuration instance. I'm not sure if you already
have some shared container object (like a Spring context) that exposes
singletons. If not, you can share the singleton Configuration through a
static field though. My point is that this is the duty of the ObjectWrapper
instance, not of a global static registry. Well, the static registry surely
works in your application. But, the idea of registering custom
"sub-wrappers" under DefaultObjectWrapper came up in the past, although not
to work around OSGi complications, but so that people can extend
DefaultObjectWrapper (or some other stock subclass of it -
ExtendableDefaultObjectWrapper or such) without actually extending the
class. Then it's possible to "extend" the stock ObjectWrapper on runtime.
That could also help with this OSGi issue.

One issue I spotted in above (possibly simplified) code is that you just
look up the concrete class in the Map of the registry. Subclasses can break
that lookup. It's more likely that you want to look up the class, and if
that fails then the superclass of it, and so on. And then there are also
the implemented interfaces that should be looked up.

As of the need for custom classes like  JsonPrimitiveFreemarkerWrapper, I
think that's only necessary if you need some special functionality, like
exposing a JsonObject as a hash (consider XML DOM wrapper in FreeMarker as
an example). Otherwise white-listing should be able to take care of the
security aspect. I guess at least. That though will still require the
bundle to add its own white-list rules to the ObjectWrapper, but that's not
a "sub-wrapper", just some list of methods. Actually, I'm not sure what
JsonPrimitiveFreemarkerWrapper does. Is it a TemplateModel?

Note that my goal is that commonly needed functionality goes into
FreeMarker, and being able to add wrapping rules dynamically (i.e. without
subclassing the stock ObjectWrapper) is a such thing. White-listing based
security is too, though that's a different topic.

BTW, you did write to the mailing list already (if you check the address
you will see). No problem, just saying.

On Wed, Oct 30, 2019 at 11:40 AM Christoph Rüger <c....@synesty.com>
wrote:

> Hi Daniel,
> based on our recent conversation about the security issue, we have
> continued adding some things and I wanted to bring this up. Maybe something
> is worth considering to add it to Freemarker...
>
>
> *OSGI and Model Wrapping*
> In an OSGI Application each bundle has a different classloader. In our case
> BundleA can objects to the Freemarker Context and theoretically an object
> of ClassA could become accessible (through a chain of getters when using
> BeanModel). Our ObjectWrapper resides in BundleB and cannot see ClassA of
> BundleA. Our CustomObjectWrapper lives in BundleB.
>
> That's why we looked for a way how BundleA can register a custom
> WrappingCallback which is used by our CustomObjectWrapper in BundleB.
>
> Here is what we came up with:
>
> BundleA -> calls FMRegistry.registerCustomObjectWrapper(new
> CustomFreemarkerJsonWrapper())
>
> BundleB.CustomObjectWrapper --> calls FMRegistry.wrap(object)
>
> BundleB FMRegistry looks like this:
>
>
> /**
>
> * Registers a custom Object Wrapper for Freemarker
>
> * object wrapping.
>
> * This can be used by other bundles to provide
>
> * custom wrappers for classes which are not known or visible here.
>
> *
>
> * *@param* clazz
>
> * *@param* customObjectWrapper
>
> */
>
> *public* static *void*
> registerCustomObjectWrapper(CustomFreemarkerObjectWrapperCallback
> customObjectWrapperCallback) {
>
> List<Class> supportedTypes = customObjectWrapperCallback
> .getSupportedTypes(); // e.g. Arrays.asList(JsonPrimitive.class,
> JsonArray.class);
>
> *for* (Class clazz : supportedTypes) {
>
> *if*(!*this*.customFreemarkerObjectWrappers.containsKey(clazz)) {
>
> *this*.customFreemarkerObjectWrappers.put(clazz,
> customObjectWrapperCallback
> );
>
> }
>
> }
>
> }
>
>
>
> /**
>
> * Used by {@link MyObjectWrapper} to wrap
>
> * unknown types which were registered by {@link
> SynestyTemplateMethodModelWithCustomObjectWrapper}
>
> *
>
> * *@param* obj
>
> * *@return* a callback if found, or <code>null</code> if no callback was
> registered for the given class (by obj.getClass())
>
> */
>
> *public* CustomFreemarkerObjectWrapperCallback wrap(Object obj) {
>
> *if*(obj == *null*) *return* *null*;
>
> *return* *this*.customFreemarkerObjectWrappers.get(obj.getClass());
>
> }
>
>
> Then in our CustomObjectWrapper in BundleB we do something like this
> .wrap(obj) or .handleUnknownType(obj)
>
> // check if a customObjectWrapper is registered
>
>         BiFunction<Object, BeansWrapper, Object> customWrapper =
> FMRegistry.wrap(object);
>
>         *if*(customWrapper != *null*) {
>
>
>
>         Object wrappedObject = customWrapper.apply(object, *this*);
>
>         *if*(wrappedObject != *null*) {
>
>         *return* wrap(wrappedObject);
>
>         }
>
>         }
>
> /* ... */
> return super.wrap(object) // or
>
>
> Here is an example of such a CallbackWrapper in BundleA:
>
> /**
>
>  * A custom ObjectWrapper Callback for GSON Json Objects. Callback because
> it
>
>  * will be called by our {@link MyObjectWrapper} during wrapping phase on
> the
>
>  * fly.
>
>  *
>
>  * e.g. to be able to convert gson-JsonArray into a ArrayList (instead of
>
>  * Iterable) so that we can access it in Freemarker as a sequence e.g.
> seq[0],
>
>  * seq[1] etc.
>
>  *
>
>  */
>
> *public* *class* CustomFreemarkerJsonWrapper *implements*
> CustomFreemarkerObjectWrapperCallback {
>
>
> /**
>
> * Returns the classes which this wrapper supports.
>
> *
>
> * *@return*
>
> */
>
> @Override
>
> *public* List<Class> getSupportedTypes() {
>
> *return* Arrays.*asList*(JsonPrimitive.*class*, JsonArray.*class*);
>
> }
>
>
> @Override
>
> *public* Object apply(Object t, BeansWrapper beansWrapper) {
>
> *if* (t *instanceof* JsonArray) {
>
>
> JsonArray asJsonArray = (JsonArray) t;
>
> *return* *new* JsonArrayFreemarkerWrapper(asJsonArray, beansWrapper);
>
> } *else* *if* (t *instanceof* JsonPrimitive) {
>
> *return* *new* JsonPrimitiveFreemarkerWrapper((JsonPrimitive) t,
> beansWrapper);
>
> } *else* {
>
> *return* t;
>
> }
>
> }
>
>
> }
>
> *To conclude:*
> FMRegistry allows several bundles to register a custom objectWrapping
> method for different classes.
> Our custom ObjectWrapper can use those callbacks to wrap the objects which
> are unknown to its own Classloader.
>
> This allows us to control ObjectWrapping in different bundles and solve
> some classloader issues and avoid direct dependencies of bundles.
>
> This is also needed to solve our security issue, as you suggested to always
> build own WrapperModels for "dangerous" objects.
>
>  I will write another mail regarding more the security aspect, which is
> related but would be too much for this part.
>
>
> Let me know if you think the above makes sense. I can also bring it up in
> the mailing list, but first wanted to check with you directly.
>
> Thanks
> Christoph
>
>
>
>
> --
> Christoph Rüger, Geschäftsführer
> Synesty <https://synesty.com/> - Anbinden und Automatisieren ohne
> Programmieren
>
> --
> Synesty GmbH
> Moritz-von-Rohr-Str. 1a
> 07745 Jena
> Tel.: +49 3641
> 5596493Internet: https://synesty.com <https://synesty.com>
> Informationen
> zum Datenschutz: https://synesty.com/datenschutz
> <https://synesty.com/datenschutz>
>
> Geschäftsführer: Christoph Rüger
>
> Unternehmenssitz: Jena
> Handelsregister B beim Amtsgericht: Jena
>
> Handelsregister-Nummer: HRB 508766
> Ust-IdNr.: DE287564982
>


-- 
Best regards,
Daniel Dekany