Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use serialization filter (ObjectInputerFilter) with Keycloak adapter and Memcached

I'm using Spring Security Keycloak adapter 12.0.1 and Memcached for session replication. When the session is loaded from Memcached, classes from the Keyclaok adapter are deserialized. The read method of the class KeycloakSecurityContext contains

DelegatingSerializationFilter.builder()
    .addAllowedClass(KeycloakSecurityContext.class)
    .setFilter(in);

...which sets an ObjectFilter for the current ObjectInputStream.

I found out that I have to set the system property jdk.serialSetFilterAfterRead to true, otherwise the exception filter can not be set after an object has been read is thrown and DelegatingSerializationFilter complains that it is not possible to set the object filter. The result is that no object filter is applied at all and the log is spammed with warnings.

After applying jdk.serialSetFilterAfterRead, I encountered that the ObjectInputStream with the memcached attributes contains further classes that are not set as allowed classes from the DelegatingSerializationFilter, e.g.:

org.springframework.security.web.savedrequest.DefaultSavedRequest

The result is that these classes are rejected during the serialization process.

So my question is this: Does anybody know how to configure the object filter so that serialization is working correctly?

like image 230
f1l2 Avatar asked Mar 11 '21 13:03

f1l2


3 Answers

What is happening

Your problem is that memcached session handler is deserializing a bunch of different classes at the same time. That is, in the same ObjectInputStream. This happens because your (Tomcat? or whatever application server, it doesn't matter) session is composed by a number of different objects; so memcached serializes them one-by-one into its storage and then, the same way, they are deserialized back into your application. This works great until you deserialize an object of a bitchy class, KeycloakSecurityContext, that introduces an evil filter ๐Ÿ˜ฑ in your ObjectInputStream. Link to the evil class.

What happens after that is that the keycloak class is allowed so it keeps being deserialized correctly, but now magically all other classes are not allowed because keycloak excludes all of them by adding "!*" at the end of its filter. Link to the filter.

Now you have a strong headache ๐Ÿ˜ต and you're thinking "Enough bullshit, how do I fix this?". I understand and I've got a bad headache as well, keep reading.


Solution

There are multiple solutions to this issue (good news! ๐Ÿ˜€) depending on how deep you dare (๐Ÿ˜ฃ) go and change things in your application.

The correctโ„ข solution is to make sure that KeycloakSecurityContext objects (and any other class that introduces such filters) are deserialized in their own streams rather than in the same, common, stream. Now, I'm not an expert in this memcached session handler thing so I don't know how much control you actually have of the whole session deserialization process; but I'm quite confident that it should be possible to hack into it.

If this, for some reason, is not possible, you need to override the filter in KeycloakSecurityContext by extending the class. If you're choosing this path (fly, you fools ๐Ÿ˜ˆ) you have to play with the filter and decide what to do. The approach that I would think it's most correctโ„ข in this case is to completely remove the filter thing from KeycloakSecurityContext and then add a filter at the application level where you define all the classes and the packages that you allow for deserialization in your application. Link on how to do it.

Another approach, a bit hacky but more immediate, is to add all the relevant classes in the filter in the class extending KeycloakSecurityContext (seriously, don't do this).


Bonus good-citizen point

Some might even argue that this is a bug, or let's say a missing functionality, in memcached-session-manager. Might be worth opening an issue there and see what they think about it. https://github.com/magro/memcached-session-manager


Now the hardcore stuff

In-depth practical explanation of the problem for nerds who have nothing to do.

Think about the following scenario: you have two types of classes, A and B, and both classes are Serializable.

Class A is a simple, a bit naรฏve, class and doesn't add any custom filters in its readObject method. In the same ObjectInputStream you deserialize the objects: a1, a2, a3, a4. All instances of class A. All objects are deserialized correctly, fuck yeah! ๐Ÿ’

Class B, instead, has a very mean filter in its readObject that allows only objects of the same class B to be deserialized in the current ObjectInputStream.

Now we read the following objects in a stream: a1, b1, a2, b2.

  • a1 is deserialized correctly, there is no filter right now in our ObjectInputStream, great!
  • b1 is deserialized correctly, cool; but b1 is also a little bit of a bitch (being an instance of Class B) so it sets a new filter for our beloved stream and class B is, from now on, the only class allowed for deserialization
  • then comes a2 and it is not deserialized (whaaaat ๐Ÿ˜ฉ), because b1, in the previous step, set a filter that doesn't include class A and this is all happening in the same ObjectInputStream instance
  • finally, b2 is deserialized correctly because Class B is included in the filter

like image 77
Andy Valerio Avatar answered Sep 27 '22 22:09

Andy Valerio


The code implemented in KeycloakSecurityContext and other related classes was included in order to mitigate a bug associated with the CVE CVE-2020-1714:

A flaw was found in Keycloak, where the code base contains usages of ObjectInputStream without type checks. This flaw allows an attacker to inject arbitrarily serialized Java Objects, which would then get deserialized in a privileged context and potentially lead to remote code execution.

The solution tries to address the above-mentioned vulnerability by implementing a custom ObjectInputFilter.

As indicated in the bug description, the idea behind the Java serialization filtering mechanism is to prevent deserialization vulnerabilities that can lead to remote code execution that can cause a security issue to the application.

In many situations, like when dealing with sessions, several objects, those stored in the session following the example, are serialized and will be later deserialized together, in other words, they will be written to the same ObjectOutputStream and then they will be read from the same ObjectInputStream.

When a ObjectInputFilter is applied, which can be done at several levels - process, application and specific ObjectInputStream - only the objects that satisfy the configured filter pattern will be deserialized; depending on the pattern itself, the rest will be either rejected or the decision will be delegated to a process-wide filter if one exists.

Please, consider the following example, where A, B, C and D are classes, and a filter A;D;!* is applied over an hypothetical object input stream, represented by the top marble diagram line, being the bottom one the deseriaization result after the filter is applied:

ObjectInputFilter semantics

This pattern can be defined in terms of modules, packages and/or individual classes, and can be set as the jdk.serialFilter system property, or by editing the java.security properties file.

You can create custom filters as well. They are implemented using the API provided by ObjectInputFilter, and allow for a more granular serialization control because they can be specific to a specific ObjectInputStream.

Please, see the relevant Oracle Serialization documentation for more information.

The Keycloak serialization filters use the utility class DelegatingSerializationFilter.

In the implementation provided, the filter is applied inside the KeycloakSecurityContext readObject method:

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    DelegatingSerializationFilter.builder()
            .addAllowedClass(KeycloakSecurityContext.class)
            .setFilter(in);
    in.defaultReadObject();

    token = parseToken(tokenString, AccessToken.class);
    idToken = parseToken(idTokenString, IDToken.class);
}

As a consequence, in order to work it properly, as you pointed out, it is necessary to define the jdk.serialSetFilterAfterRead system property as true when running your program.

This filter will be always applied unless a previous, non process-wide filter (see the section labeled Setting a Process-Wide Custom Filter in the Oracle documentation), has been already applied to the ObjectInputStream; it will be guaranteed by the setObjectInputFilter method and it is checked by the DelegatingSerializationFilter class as well:

private void setFilter(ObjectInputStream ois, String filterPattern) {
    LOG.debug("Using: " + serializationFilterAdapter.getClass().getSimpleName());

    if (serializationFilterAdapter.getObjectInputFilter(ois) == null) {
        serializationFilterAdapter.setObjectInputFilter(ois, filterPattern);
    }
}

In other words, the only way to avoid a custom filter to be applied is to provide first an initial filter over the target ObjectInputStream, the one that contains your session data this time. As indicated in the docs:

The filter mechanism is called for each new object in the stream. If more than one active filter (process-wide filter, application filter, or stream-specific filter) exists, only the most specific filter is called.

The way in which the creation of this initial filter should be accomplished is highly dependent on the code that is actually dealing with the deserialization functionality.

In your use case, you probably are using memcached-session-manager, either the original version or some more updated projects in Github.

In a normal use case, the sessions in memcached-session-manager are mainly handled by the code defined in MemcachedSessionService.

This class uses TranscoderService for handling the Java serialization stuff.

TranscoderService in turn delegates that responsability to a proper implementation of TranscoderFactory and SessionAttributesTranscoder.

JavaSerializationTranscoderFactory and the associated class JavaSerializationTranscoder are the default implementations of these interfaces.

Please, pay attention to the deserializeAttributes method of JavaSerializationTranscoder, it defines the logic for session deserialization:

/**
  * Get the object represented by the given serialized bytes.
  *
  * @param in
  *            the bytes to deserialize
  * @return the resulting object
  */
@Override
public ConcurrentMap<String, Object> deserializeAttributes(final byte[] in ) {
    ByteArrayInputStream bis = null;
    ObjectInputStream ois = null;
    try {
        bis = new ByteArrayInputStream( in );
        ois = createObjectInputStream( bis );

        final ConcurrentMap<String, Object> attributes = new ConcurrentHashMap<String, Object>();
        final int n = ( (Integer) ois.readObject() ).intValue();
        for ( int i = 0; i < n; i++ ) {
            final String name = (String) ois.readObject();
            final Object value = ois.readObject();
            if ( ( value instanceof String ) && ( value.equals( NOT_SERIALIZED ) ) ) {
                continue;
            }
            if ( LOG.isDebugEnabled() ) {
                LOG.debug( "  loading attribute '" + name + "' with value '" + value + "'" );
            }
            attributes.put( name, value );
        }

        return attributes;
    } catch ( final ClassNotFoundException e ) {
        LOG.warn( "Caught CNFE decoding "+ in.length +" bytes of data", e );
        throw new TranscoderDeserializationException( "Caught CNFE decoding data", e );
    } catch ( final IOException e ) {
        LOG.warn( "Caught IOException decoding "+ in.length +" bytes of data", e );
        throw new TranscoderDeserializationException( "Caught IOException decoding data", e );
    } finally {
        closeSilently( bis );
        closeSilently( ois );
    }
}

As you can see, the problem is that the session information, represented by the input byte array, can contain several attributes, and all of them are deserialized from the same ObjectInputStream. Once the Keycloak ObjectInputFilter is applied on this ObjectInputStream, as you indicated, it will reject the rest of classes that are not allowed by the filter. The reason is that DelegatingSerializationFilter append a final !* to the filter pattern that is being constructed, excluding everything but the explicitly provided class and text based patterns (well, and the classes of java.util.* to allow collections).

In order to avoid this problem, try providing your own implementation of SessionAttributesTranscoder, and include a method something similar to deserializeAttributes but defining an initial filter over the constructed ObjectInputStream.

For example (please, forgive to define the whole class, you can probably reuse the code of JavaSerializationTranscoder in a certain way):

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.catalina.session.StandardSession;
import org.apache.catalina.util.CustomObjectInputStream;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;

import de.javakaffee.web.msm.MemcachedSessionService.SessionManager;

public class CustomJavaSerializationTranscoder implements SessionAttributesTranscoder {

    private static final Log LOG = LogFactory.getLog( CustomJavaSerializationTranscoder.class );

    private static final String EMPTY_ARRAY[] = new String[0];

    /**
     * The dummy attribute value serialized when a NotSerializableException is
     * encountered in <code>writeObject()</code>.
     */
    protected static final String NOT_SERIALIZED = "___NOT_SERIALIZABLE_EXCEPTION___";

    private final SessionManager _manager;

    /**
     * Constructor.
     *
     * @param manager
     *            the manager
     */
    public CustomJavaSerializationTranscoder() {
        this( null );
    }

    /**
     * Constructor.
     *
     * @param manager
     *            the manager
     */
    public CustomJavaSerializationTranscoder( final SessionManager manager ) {
        _manager = manager;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public byte[] serializeAttributes( final MemcachedBackupSession session, final ConcurrentMap<String, Object> attributes ) {
        if ( attributes == null ) {
            throw new NullPointerException( "Can't serialize null" );
        }

        ByteArrayOutputStream bos = null;
        ObjectOutputStream oos = null;
        try {
            bos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream( bos );

            writeAttributes( session, attributes, oos );

            return bos.toByteArray();
        } catch ( final IOException e ) {
            throw new IllegalArgumentException( "Non-serializable object", e );
        } finally {
            closeSilently( bos );
            closeSilently( oos );
        }

    }

    private void writeAttributes( final MemcachedBackupSession session, final Map<String, Object> attributes,
            final ObjectOutputStream oos ) throws IOException {

        // Accumulate the names of serializable and non-serializable attributes
        final String keys[] = attributes.keySet().toArray( EMPTY_ARRAY );
        final List<String> saveNames = new ArrayList<String>();
        final List<Object> saveValues = new ArrayList<Object>();
        for ( int i = 0; i < keys.length; i++ ) {
            final Object value = attributes.get( keys[i] );
            if ( value == null || session.exclude( keys[i], value ) ) {
                continue;
            } else if ( value instanceof Serializable ) {
                saveNames.add( keys[i] );
                saveValues.add( value );
            } else {
                if ( LOG.isDebugEnabled() ) {
                    LOG.debug( "Ignoring attribute '" + keys[i] + "' as it does not implement Serializable" );
                }
            }
        }

        // Serialize the attribute count and the Serializable attributes
        final int n = saveNames.size();
        oos.writeObject( Integer.valueOf( n ) );
        for ( int i = 0; i < n; i++ ) {
            oos.writeObject( saveNames.get( i ) );
            try {
                oos.writeObject( saveValues.get( i ) );
                if ( LOG.isDebugEnabled() ) {
                    LOG.debug( "  storing attribute '" + saveNames.get( i ) + "' with value '" + saveValues.get( i ) + "'" );
                }
            } catch ( final NotSerializableException e ) {
                LOG.warn( _manager.getString( "standardSession.notSerializable", saveNames.get( i ), session.getIdInternal() ), e );
                oos.writeObject( NOT_SERIALIZED );
                if ( LOG.isDebugEnabled() ) {
                    LOG.debug( "  storing attribute '" + saveNames.get( i ) + "' with value NOT_SERIALIZED" );
                }
            }
        }

    }

    /**
     * Get the object represented by the given serialized bytes.
     *
     * @param in
     *            the bytes to deserialize
     * @return the resulting object
     */
    @Override
    public ConcurrentMap<String, Object> deserializeAttributes(final byte[] in ) {
        ByteArrayInputStream bis = null;
        ObjectInputStream ois = null;
        try {
            bis = new ByteArrayInputStream( in );
            ois = createObjectInputStream( bis );

            // Fix deserialization
            fixDeserialization(ois);

            final ConcurrentMap<String, Object> attributes = new ConcurrentHashMap<String, Object>();
            final int n = ( (Integer) ois.readObject() ).intValue();
            for ( int i = 0; i < n; i++ ) {
                final String name = (String) ois.readObject();
                final Object value = ois.readObject();
                if ( ( value instanceof String ) && ( value.equals( NOT_SERIALIZED ) ) ) {
                    continue;
                }
                if ( LOG.isDebugEnabled() ) {
                    LOG.debug( "  loading attribute '" + name + "' with value '" + value + "'" );
                }
                attributes.put( name, value );
            }

            return attributes;
        } catch ( final ClassNotFoundException e ) {
            LOG.warn( "Caught CNFE decoding "+ in.length +" bytes of data", e );
            throw new TranscoderDeserializationException( "Caught CNFE decoding data", e );
        } catch ( final IOException e ) {
            LOG.warn( "Caught IOException decoding "+ in.length +" bytes of data", e );
            throw new TranscoderDeserializationException( "Caught IOException decoding data", e );
        } finally {
            closeSilently( bis );
            closeSilently( ois );
        }
    }

    private ObjectInputStream createObjectInputStream( final ByteArrayInputStream bis ) throws IOException {
        final ObjectInputStream ois;
        ClassLoader classLoader = null;
        if ( _manager != null && _manager.getContext() != null ) {
            classLoader = _manager.getContainerClassLoader();
        }
        if ( classLoader != null ) {
            ois = new CustomObjectInputStream( bis, classLoader );
        } else {
            ois = new ObjectInputStream( bis );
        }
        return ois;
    }

    private void closeSilently( final OutputStream os ) {
        if ( os != null ) {
            try {
                os.close();
            } catch ( final IOException f ) {
                // fail silently
            }
        }
    }

    private void closeSilently( final InputStream is ) {
        if ( is != null ) {
            try {
                is.close();
            } catch ( final IOException f ) {
                // fail silently
            }
        }
    }

    // Helper method, reusing the `DelegatingSerializationFilter` class, which in fact is convenient because of its portability
    // accross JDK versions, to define an allow everything pattern
    // Probably it should be improved to restrict to certain patterns to
    // prevent security vulnerabilities
    private void fixDeserialization(ObjectInputStream ois) {
      DelegatingSerializationFilter.builder()
          .addAllowedPattern("*")
          .setFilter(ois);
    }

}

Now, define a custom TranscoderFactory. Let's reuse the code of the class JavaSerializationTranscoderFactory this time:

import de.javakaffee.web.msm.MemcachedSessionService.SessionManager;

public class CustomJavaSerializationTranscoderFactory extends JavaSerializationTranscoderFactory {

    /**
     * {@inheritDoc}
     */
    @Override
    public SessionAttributesTranscoder createTranscoder( final SessionManager manager ) {
        return new CustomJavaSerializationTranscoder( manager );
    }

}

Place this classes on your classpath, with the rest of libraries from memcached-session-manager, and provide a convenient value for the transcoderFactoryClass memcached-session-manager configuration property, as indicated in the docs:

The class name of the factory that creates the transcoder to use for serializing/deserializing sessions to/from memcached. The specified class must implement de.javakaffee.web.msm.TranscoderFactory and provide a no-args constructor.

I have no the ability to test the solution, although a simple test seems to work properly:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class SerializationTest {

  private static final Log LOG = LogFactory.getLog( SerializationTest.class );

  protected static final String NOT_SERIALIZED = "___NOT_SERIALIZABLE_EXCEPTION___";

  private static final String EMPTY_ARRAY[] = new String[0];

  public byte[] serializeAttributes( final ConcurrentMap<String, Object> attributes ) {
    if ( attributes == null ) {
      throw new NullPointerException( "Can't serialize null" );
    }

    ByteArrayOutputStream bos = null;
    ObjectOutputStream oos = null;
    try {
      bos = new ByteArrayOutputStream();
      oos = new ObjectOutputStream( bos );

      writeAttributes( attributes, oos );

      return bos.toByteArray();
    } catch ( final IOException e ) {
      throw new IllegalArgumentException( "Non-serializable object", e );
    } finally {
      closeSilently( bos );
      closeSilently( oos );
    }

  }

  private void writeAttributes(final Map<String, Object> attributes, final ObjectOutputStream oos ) throws IOException {

    // Accumulate the names of serializable and non-serializable attributes
    final String keys[] = attributes.keySet().toArray( EMPTY_ARRAY );
    final List<String> saveNames = new ArrayList<String>();
    final List<Object> saveValues = new ArrayList<Object>();
    for ( int i = 0; i < keys.length; i++ ) {
      final Object value = attributes.get( keys[i] );
      if ( value == null ) {
        continue;
      } else if ( value instanceof Serializable) {
        saveNames.add( keys[i] );
        saveValues.add( value );
      } else {
        if ( LOG.isDebugEnabled() ) {
          LOG.debug( "Ignoring attribute '" + keys[i] + "' as it does not implement Serializable" );
        }
      }
    }

    // Serialize the attribute count and the Serializable attributes
    final int n = saveNames.size();
    oos.writeObject( Integer.valueOf( n ) );
    for ( int i = 0; i < n; i++ ) {
      oos.writeObject( saveNames.get( i ) );
      try {
        oos.writeObject( saveValues.get( i ) );
        if ( LOG.isDebugEnabled() ) {
          LOG.debug( "  storing attribute '" + saveNames.get( i ) + "' with value '" + saveValues.get( i ) + "'" );
        }
      } catch ( final NotSerializableException e ) {
        LOG.warn(  "standardSession.notSerializable" + saveNames.get( i ), e );
        oos.writeObject( NOT_SERIALIZED );
        if ( LOG.isDebugEnabled() ) {
          LOG.debug( "  storing attribute '" + saveNames.get( i ) + "' with value NOT_SERIALIZED" );
        }
      }
    }

  }

  public ConcurrentMap<String, Object> deserializeAttributes(final byte[] in ) {
    ByteArrayInputStream bis = null;
    ObjectInputStream ois = null;
    try {
      bis = new ByteArrayInputStream( in );
      ois = new ObjectInputStream( bis );

      fixDeserialization(ois);

      final ConcurrentMap<String, Object> attributes = new ConcurrentHashMap<String, Object>();
      final int n = ( (Integer) ois.readObject() ).intValue();
      for ( int i = 0; i < n; i++ ) {
        final String name = (String) ois.readObject();
        final Object value = ois.readObject();
        if ( ( value instanceof String ) && ( value.equals( NOT_SERIALIZED ) ) ) {
          continue;
        }
        if ( LOG.isDebugEnabled() ) {
          LOG.debug( "  loading attribute '" + name + "' with value '" + value + "'" );
        }
        attributes.put( name, value );
      }

      return attributes;
    } catch ( final ClassNotFoundException e ) {
      LOG.warn( "Caught CNFE decoding "+ in.length +" bytes of data", e );
      throw new RuntimeException( "Caught CNFE decoding data", e );
    } catch ( final IOException e ) {
      LOG.warn( "Caught IOException decoding "+ in.length +" bytes of data", e );
      throw new RuntimeException( "Caught IOException decoding data", e );
    } finally {
      closeSilently( bis );
      closeSilently( ois );
    }
  }

  private void fixDeserialization(ObjectInputStream ois) {
    DelegatingSerializationFilter.builder()
        .addAllowedPattern("*")
        .setFilter(ois);
  }

  private void closeSilently( final OutputStream os ) {
    if ( os != null ) {
      try {
        os.close();
      } catch ( final IOException f ) {
        // fail silently
      }
    }
  }

  private void closeSilently( final InputStream is ) {
    if ( is != null ) {
      try {
        is.close();
      } catch ( final IOException f ) {
        // fail silently
      }
    }
  }

  public static void main(String[] args) throws Exception{

    Person person = new Person("Sherlock Holmes","Consulting detective");
    Address address = new Address("221B Baker Street");
    ConcurrentMap<String, Object> attributes = new ConcurrentHashMap<String, Object>();
    attributes.put("person", person);
    attributes.put("address", address);

    SerializationTest test = new SerializationTest();
    byte[] in = test.serializeAttributes(attributes);

    System.setProperty("jdk.serialSetFilterAfterRead", "true");

    ConcurrentMap<String, Object> attributesAfter = test.deserializeAttributes(in);
    System.out.println(attributesAfter);
  }
}

Person and Address two simple POJOs. Pay attention to the definition Person readObject method:

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Person implements Serializable {

  private static final long serialVersionUID = 1L;

  String name;
  String title;

  public Person() {
  }

  public Person(String name, String title) {
    this.name = name;
    this.title = title;
  }

  private void writeObject(ObjectOutputStream out) throws IOException {
    out.defaultWriteObject();
  }

  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    DelegatingSerializationFilter.builder()
        .addAllowedClass(Person.class)
        .setFilter(in);
    in.defaultReadObject();
  }

  @Override
  public String toString() {
    return "Person{" +
        "name='" + name + '\'' +
        ", title='" + title + '\'' +
        '}';
  }
}
import java.io.Serializable;

public class Address implements Serializable {

  private static final long serialVersionUID = 1L;

  String address;

  public Address() {
  }

  public Address(String address) {
    this.address = address;
  }

  @Override
  public String toString() {
    return "Address{" +
        "address='" + address + '\'' +
        '}';
  }
}

Please, be aware that the filter is there in order to prevent security flaws: as also suggested in the code comments, it would be advisable to improve the logic implemented in the fixDeserialization to restrict in some way the possible classes that your session is supposed to contain instead of use the wildcard * as in the example.

In fact, this functionality can be included in the memcached-session-manager library, probably by defining some kind of configuration property, serialFilter, for instance, which value, a valid filter pattern, should be provided to the indicated underlying Java deserialization mechanisms.

I created a fork of the project: it is still a WIP, but please, see this commit, I hope you get the idea. I will try to pull a request to the forked repo once finished.

like image 25
jccampanero Avatar answered Sep 27 '22 23:09

jccampanero


I know this question is regarding Memcached, but I came here with the exact same error using Spring Session w/ Hazelcast. I will post what I found in case it helps others.

Basically, Spring Session Hazelcast 2.4.2 provides HazelcastSessionSerializer. I was previously using 2.3.2 which does not have this class, and defaults to Java serialization, resulting in the "rejected" error when deserializing DefaultSavedRequest.

The configuration example includes a few new lines to set this serializer:

SerializerConfig serializerConfig = new SerializerConfig();
serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
config.getSerializationConfig().addSerializerConfig(serializerConfig);

This resolved the issue for me.

like image 28
Luke Avatar answered Sep 28 '22 00:09

Luke