将序列化过滤器 (ObjectInputerFilter) 与 Keycloak 适配器和 Memcached 结合使用

f1l*_*1l2 9 java serialization memcached keycloak

我使用 Spring Security Keycloak 适配器 12.0.1 和 Memcached 进行会话复制。当会话从 Memcached 加载时,来自 Keyclaok 适配器的类被反序列化。该类的读取方法KeycloakSecurityContext包含

DelegatingSerializationFilter.builder()
    .addAllowedClass(KeycloakSecurityContext.class)
    .setFilter(in);
Run Code Online (Sandbox Code Playgroud)

...ObjectFilter为当前ObjectInputStream.

我发现我必须将系统属性设置jdk.serialSetFilterAfterRead为true,否则filter can not be set after an object has been read会抛出异常并DelegatingSerializationFilter抱怨无法设置对象过滤器。结果是根本没有应用任何对象过滤器,并且日志中充斥着警告。

申请后jdk.serialSetFilterAfterRead,我遇到ObjectInputStream具有 memcached 属性的 包含未设置为允许的类的更多类DelegatingSerializationFilter,例如:

org.springframework.security.web.savedrequest.DefaultSavedRequest
Run Code Online (Sandbox Code Playgroud)

结果是这些类在序列化过程中被拒绝。

所以我的问题是:有人知道如何配置对象过滤器以便序列化正常工作吗?

Giu*_*rio 12

怎么了

您的问题是memcached会话处理程序同时反序列化一堆不同的类。也就是说,在同一个ObjectInputStream. 发生这种情况是因为您的(Tomcat?或任何应用程序服务器,没关系)会话由许多不同的对象组成;因此memcached将它们一一序列化到其存储中,然后以同样的方式将它们反序列化回您的应用程序。这很有效,直到您反序列化一个讨厌的类的对象,KeycloakSecurityContext,在您的ObjectInputStream. 链接到邪恶的类

之后发生的事情是,keycloak 类被允许,因此它一直被正确地反序列化,但现在神奇的是,所有其他类都被禁止了,因为 keycloak 通过在其过滤器的末尾添加“!*”来排除所有这些链接到过滤器

现在你有一个强烈的头痛,你在想“废话太多了,我该如何解决这个问题?”。我明白了,我也头疼得很厉害,请继续阅读。


解决方案

这个问题有多种解决方案(好消息!),这取决于你敢 () 去改变你的应用程序的内容。

正确的™ 解决方案是确保KeycloakSecurityContext对象(以及引入此类过滤器的任何其他类)在它们自己的流中而不是在相同的公共流中反序列化。现在,我不是这个memcached会话处理程序方面的专家,所以我不知道您实际上对整个会话反序列化过程有多少控制;但我非常有信心入侵它应该是可能的。

如果由于某种原因这是不可能的,您需要KeycloakSecurityContext通过扩展类来覆盖过滤器。如果你选择这条路(飞,你这个傻瓜),你必须玩弄过滤器并决定做什么。在这种情况下,我认为最正确的方法是完全删除过滤器KeycloakSecurityContext,然后在应用程序级别添加一个过滤器,您可以在其中定义允许在应用程序中反序列化的所有类和包有关如何操作的链接

另一种有点老套但更直接的方法是将过滤器中的所有相关类添加到扩展类中KeycloakSecurityContext(说真的,不要这样做)。


奖励好公民积分

有些人甚至可能会争辩说,这是一个错误,或者比方说在memcached-session-manager. 可能值得在那里打开一个问题,看看他们是怎么想的。 https://github.com/magro/memcached-session-manager


现在是铁杆的东西

为无所事事的书呆子们深入实践的问题解说。

考虑以下场景:您有两种类型的类AB,并且两个类都是Serializable

ClassA是一个简单的、有点幼稚的类,并且没有在其readObject方法中添加任何自定义过滤器。同样,ObjectInputStream您反序列化对象:a1, a2, a3, a4. 类的所有实例A。所有对象都被正确反序列化,他妈的耶!

B相反,Class有一个非常平均的过滤器,readObject它只允许B在当前ObjectInputStream.

现在我们看在流以下对象:a1, b1, a2, b2

  • a1已正确反序列化,我们的 中现在没有过滤器ObjectInputStream,太棒了!
  • b1正确反序列化,很酷;但b1有点小婊子(作为 Class 的一个实例B)所以它为我们心爱的流设置了一个新的过滤器,从现在开始,B 类是唯一允许反序列化的类
  • 然后来了a2,它没有被反序列化(whaaaat),因为b1在上一步中,设置了一个不包含 A 类的过滤器,一切都发生在同一个ObjectInputStream实例中
  • 最后,b2 被正确反序列化,因为 B 类包含在过滤器中


jcc*_*ero 7

包含在KeycloakSecurityContext和其他相关类中实现的代码是为了减轻与 CVE 相关的错误CVE-2020-1714

在 Keycloak 中发现了一个缺陷,其中代码库包含 ObjectInputStream 的用法而没有类型检查。此漏洞允许攻击者注入任意序列化的 Java 对象,然后在特权上下文中反序列化并可能导致远程代码执行。

该解决方案试图通过实现自定义ObjectInputFilter.

正如错误描述中所指出的,Java 序列化过滤机制背后的想法是防止反序列化漏洞,这些漏洞可能导致远程代码执行,从而导致应用程序出现安全问题。

在许多情况下,例如在处理会话时,存储在示例中的会话中的多个对象被序列化,稍后将一起反序列化,换句话说,它们将被写入相同的对象ObjectOutputStream,然后从一样ObjectInputStream

ObjectInputFilter应用a时,可以在多个级别(进程、应用程序和特定)完成ObjectInputStream,只有满足配置的过滤器模式的对象才会被反序列化;根据模式本身,其余的要么被拒绝,要么决定将委托给进程范围的过滤器(如果存在)

请,考虑下面的例子,其中ABCD是类,以及过滤器A;D;!*被施加在一个假想对象的输入流,通过顶大理石图线表示,是所述底部之一deseriaization结果应用于过滤后:

ObjectInputFilter 语义

这种模式可以根据模块、包和/或单个类来定义,并且可以设置为jdk.serialFilter系统属性,或者通过编辑java.security属性文件。

您也可以创建自定义过滤器。它们是使用 提供的 API 实现的ObjectInputFilter,并允许进行更细粒度的序列化控制,因为它们可以特定于特定的ObjectInputStream.

请参阅相关的Oracle 序列化文档以获取更多信息。

Keycloak序列化过滤器使用的实用工具类DelegatingSerializationFilter

在提供的实现中,过滤器应用于KeycloakSecurityContext readObject方法内部:

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);
}
Run Code Online (Sandbox Code Playgroud)

因此,为了正常工作,正如您所指出的,有必要在运行程序时定义jdk.serialSetFilterAfterRead系统属性true

这种过滤器将被应用于总是除非先前的,非处理宽过滤器(参见标记为部分设置进程范围自定义过滤器的Oracle的文档),已经被应用到ObjectInputStream; 它将由setObjectInputFilter方法保证,并由DelegatingSerializationFilter类检查

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

    if (serializationFilterAdapter.getObjectInputFilter(ois) == null) {
        serializationFilterAdapter.setObjectInputFilter(ois, filterPattern);
    }
}
Run Code Online (Sandbox Code Playgroud)

换句话说,避免应用自定义过滤器的唯一方法是首先在目标上提供一个初始过滤器,这次过滤器ObjectInputStream包含您的会话数据。如文档中所示

为流中的每个新对象调用过滤器机制。如果存在多个活动过滤器(进程范围过滤器、应用程序过滤器或流特定过滤器),则仅调用最具体的过滤器。

创建这个初始过滤器的方式高度依赖于实际处理反序列化功能的代码。

在您的用例中,您可能使用的memcached-session-manager原始版本或Github 中一些更新的项目

在正常用例中, 中的会话memcached-session-manager主要由 中定义的代码处理MemcachedSessionService

此类TranscoderService用于处理 Java 序列化内容。

TranscoderService反过来,将这种责任委托给正确实施TranscoderFactorySessionAttributesTranscoder

JavaSerializationTranscoderFactory和关联的类JavaSerializationTranscoder是这些接口的默认实现。

请注意 的deserializeAttributes方法JavaSerializationTranscoder,它定义了会话反序列化的逻辑:

/**
  * 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 );
    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,问题在于由输入字节数组表示的会话信息可以包含多个属性,并且所有属性都是从同一个ObjectInputStream. 正如您所指出的,一旦Keycloak ObjectInputFilter应用于 this ObjectInputStream,它将拒绝过滤器不允许的其余类。原因是DelegatingSerializationFilter final附加!*到正在构造的过滤器模式,排除除明确提供的类和基于文本的模式(以及java.util.*允许集合的类)之外的所有内容。

为了避免这个问题,请尝试提供您自己的 实现SessionAttributesTranscoder,并包含一个类似于deserializeAttributes但在构造的ObjectInputStream.

例如(请原谅定义整个类,您可能可以JavaSerializationTranscoder以某种方式重用代码):

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);
    }

}
Run Code Online (Sandbox Code Playgroud)

现在,定义一个自定义TranscoderFactory. 这次我们重用类的代码JavaSerializationTranscoderFactory

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

public class CustomJavaSerializationTranscoderFactory extends JavaSerializationTranscoderFactory {

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

}
Run Code Online (Sandbox Code Playgroud)

将这些类放在您的类路径上,其余的库来自memcached-session-manager,并为transcoderFactoryClass memcached-session-manager配置属性提供一个方便的值,如文档中所示

创建转码器以用于序列化/反序列化会话到/从 memcached 的工厂的类名称。指定的类必须实现de.javakaffee.web.msm.TranscoderFactory并提供无参数构造函数。

我没有能力测试解决方案,尽管一个简单的测试似乎可以正常工作:

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 );