Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SP-6642][PPP-4772] RCE injection via connection's JNDI database name #5780

Merged
merged 1 commit into from
Nov 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,23 @@
<test-on-borrow>true</test-on-borrow>
<test-on-return>false</test-on-return>
<pre-populate-pool>false</pre-populate-pool>
</dbcp-defaults>
</dbcp-defaults>

<!--
Allowed Datasource JNDI URL Schemes.

Comma separated list of schemes.

This property specifies the allowed schemes for datasource JNDI names with a scheme. Schemeless names are not affected.

Please, be advised that changing this property to include schemes other than `java` may pose a security risk, such
as enabling remote code execution attacks.

Examples (first is default value):
<allowed-datasource-jndi-schemes>java</allowed-datasource-jndi-schemes>
<allowed-datasource-jndi-schemes>java,other</allowed-datasource-jndi-schemes>
-->

<file-upload-defaults>
<relative-path>/system/metadata/csvfiles/</relative-path>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,6 @@

package org.pentaho.platform.engine.core.system;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.concurrent.Callable;

import org.apache.commons.collections.list.UnmodifiableList;
import org.apache.commons.lang.StringUtils;
import org.dom4j.Document;
Expand Down Expand Up @@ -67,15 +52,28 @@
import org.pentaho.platform.engine.core.system.objfac.OSGIRuntimeObjectFactory;
import org.pentaho.platform.engine.security.SecurityHelper;
import org.pentaho.platform.util.logging.Logger;
import org.pentaho.platform.util.messages.LocaleHelper;
import org.pentaho.platform.util.web.SimpleUrlFactory;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.User;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.concurrent.Callable;

@SuppressWarnings( { "rawtypes", "unchecked" } )
public class PentahoSystem {

Expand Down Expand Up @@ -203,6 +201,11 @@ public class PentahoSystem {
private static final List<String> UnmodifiableSystemDatasourcesRolesList = UnmodifiableList
.decorate( PentahoSystem.SystemDatasourcesRolesList );

private static final List<String> AllowedDatasourceJndiSchemesList = new ArrayList();

private static final List<String> UnmodifiableAllowedDatasourceJndiSchemesList = UnmodifiableList
.decorate( PentahoSystem.AllowedDatasourceJndiSchemesList );

private static final List logoutListeners = Collections.synchronizedList( new ArrayList() );

private static final IServerStatusProvider serverStatusProvider = IServerStatusProvider.LOCATOR.getProvider();
Expand Down Expand Up @@ -293,6 +296,7 @@ public static boolean init( final IApplicationContext pApplicationContext, final
if ( debug ) {
Logger.debug( PentahoSystem.class, "Reading ACL list from pentaho.xml" ); //$NON-NLS-1$
}

// Set Up ACL File Extensions by reading pentaho.xml for acl-files
//
// Read the files that are permitted to have ACLs on them from
Expand Down Expand Up @@ -322,15 +326,29 @@ public static boolean init( final IApplicationContext pApplicationContext, final
PentahoSystem.DownloadRolesList.add( st.nextToken().trim() );
}

if ( debug ) {
Logger.debug( PentahoSystem.class, "Reading System DataSources Roles from pentaho.xml" );
}
st = new StringTokenizer( PentahoSystem.getSystemSetting( "system-datasources-roles", "Administrator" ), "," );
while ( st.hasMoreElements() ) {
PentahoSystem.SystemDatasourcesRolesList.add( st.nextToken().trim() );
}

if ( debug ) {
Logger.debug( PentahoSystem.class, "Reading System DataSources from pentaho.xml" );
}
st = new StringTokenizer( PentahoSystem.getSystemSetting( "system-datasources", "Hibernate,Quartz,jackrabbit" ), "," );
while ( st.hasMoreElements() ) {
PentahoSystem.SystemDatasourcesList.add( st.nextToken().trim() );
}

if ( debug ) {
Logger.debug( PentahoSystem.class, "Reading Allowed Datasource JNDI Schemes from pentaho.xml" );
}
st = new StringTokenizer( PentahoSystem.getSystemSetting( "allowed-datasource-jndi-schemes", "java" ), "," );
while ( st.hasMoreElements() ) {
PentahoSystem.AllowedDatasourceJndiSchemesList.add( st.nextToken().trim() );
}
}

if ( debug ) {
Expand Down Expand Up @@ -1325,6 +1343,10 @@ public static List<String> getSystemDatasourcesRolesList() {
return PentahoSystem.UnmodifiableSystemDatasourcesRolesList;
}

public static List<String> getAllowedDatasourceJndiSchemesList() {
return PentahoSystem.UnmodifiableAllowedDatasourceJndiSchemesList;
}

// Stuff for the logout listener subsystem
public static void addLogoutListener( final ILogoutListener listener ) {
// add items to vector of listeners
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
package org.pentaho.platform.engine.services.connection.datasource.dbcp;

import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.dbcp2.PoolableConnection;
import org.apache.commons.dbcp2.ConnectionFactory;
import org.apache.commons.dbcp2.DriverManagerConnectionFactory;
import org.apache.commons.dbcp2.PoolableConnection;
import org.apache.commons.dbcp2.PoolableConnectionFactory;
import org.apache.commons.dbcp2.PoolingDataSource;
import org.apache.commons.lang.StringEscapeUtils;
Expand All @@ -41,10 +41,12 @@
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.annotation.Isolation;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
Expand All @@ -54,7 +56,7 @@ public class PooledDatasourceHelper {

public static PoolingDataSource setupPooledDataSource( IDatabaseConnection databaseConnection )
throws DBDatasourceServiceException {

try {
if ( databaseConnection.getAccessType().equals( DatabaseAccessType.JNDI ) ) {
throwDBDatasourceServiceException( databaseConnection.getName(), "PooledDatasourceHelper.ERROR_0008_UNABLE_TO_POOL_DATASOURCE_IT_IS_JNDI" );
Expand Down Expand Up @@ -342,60 +344,172 @@ private static void initDriverClass( DriverManagerDataSource driverManagerDataSo
}
}

public static DataSource getJndiDataSource( final String dsName ) throws DBDatasourceServiceException {
// region getJndiDataSource(..)
/**
* Looks up a data source with a given name in the JNDI naming context.
*
* @param dsName The data source name.
* @return The data source; never {@code null}.
* @throws DBDatasourceServiceException If the data source name is not from a valid namespace, or is not defined.
* @throws ClassCastException If the data source name resolves to a resource which does not implement
* {@link DataSource}.
*/
public static DataSource getJndiDataSource( String dsName ) throws DBDatasourceServiceException {
return getJndiDataSource( createJndiContext(), dsName, PentahoSystem.getAllowedDatasourceJndiSchemesList() );
}

protected static Context createJndiContext() throws DBDatasourceServiceException {
try {
InitialContext ctx = new InitialContext();
Object lkup = null;
DataSource rtn = null;
NamingException firstNe = null;
return new InitialContext();
} catch ( NamingException e ) {
throw new DBDatasourceServiceException( e );
}
}

/**
* Looks up a data source given its name in a JNDI naming context, constraining a name with scheme to be one in a
* given list.
* <p>
* The following variations of {@code dsName} are attempted in order:
* <ol>
* <li>{@code dsName}</li>
* <li>{@code "java:" + dsName}</li>
* <li>{@code "java:comp/env/jdbc/" + dsName}</li>
* <li>{@code "jdbc/" + dsName}</li>
* </ol>
*
* @param context The naming context.
* @param dsName The data source name.
* @param allowedJndiSchemes The list of JNDI schemes that a data source name with scheme may have.
* @return The data source; never {@code null}.
* @throws DBDatasourceServiceException If the data source name is not from a valid namespace, is not defined, or the
* name resolves to a resource which does not implement {@link DataSource}.
*/
public static DataSource getJndiDataSource( Context context, String dsName, List<String> allowedJndiSchemes )
throws DBDatasourceServiceException {

String[] candidateNames = {
// First, try what they ask for...
dsName,
// Need this for Jboss.
"java:" + dsName,
// Tomcat
"java:comp/env/jdbc/" + dsName,
// Others?
"jdbc/" + dsName
};

try {
return lookupJndiDataSourceByNames( context, candidateNames, allowedJndiSchemes );
} catch ( NamingException | ClassCastException e ) {
throw new DBDatasourceServiceException( e );
}
}

/**
* Looks up a data source in a naming context given its candidate names.
* <p>
* Returns the data source corresponding to the first candidate name which is from a valid namespace and is defined.
*
* @param context The naming context.
* @param candidateNames The candidate data source names. Must be non-empty.
* @param allowedJndiSchemes The list of allowed JNDI schemes of valid data source names.
* @return The data source resource; never {@code null}.
* @throws NamingException If none of the data source names are from a valid namespace or exist in the naming
* context.
* @throws ClassCastException If any of the data source names resolves to a resource which does not implement
* {@link DataSource}.
*/
private static DataSource lookupJndiDataSourceByNames( Context context,
String[] candidateNames,
List<String> allowedJndiSchemes )
throws NamingException {

assert candidateNames.length > 0;

NamingException firstNe = null;

for ( String dsName : candidateNames ) {
try {
lkup = ctx.lookup( dsName );
if ( lkup != null ) {
rtn = (DataSource) lkup;
return rtn;
}
} catch ( NamingException ignored ) {
firstNe = ignored;
}
try {
// Needed this for Jboss
lkup = ctx.lookup( "java:" + dsName ); //$NON-NLS-1$
if ( lkup != null ) {
rtn = (DataSource) lkup;
return rtn;
}
} catch ( NamingException ignored ) {
// ignored
}
try {
// Tomcat
lkup = ctx.lookup( "java:comp/env/jdbc/" + dsName ); //$NON-NLS-1$
if ( lkup != null ) {
rtn = (DataSource) lkup;
return rtn;
}
} catch ( NamingException ignored ) {
// ignored
}
try {
// Others?
lkup = ctx.lookup( "jdbc/" + dsName ); //$NON-NLS-1$
if ( lkup != null ) {
rtn = (DataSource) lkup;
return rtn;
return lookupJndiDataSourceByName( context, dsName, allowedJndiSchemes );
} catch ( NamingException e ) {
if ( firstNe == null ) {
// Keep to throw in the end if none found.
firstNe = e;
}
} catch ( NamingException ignored ) {
// ignored
}
if ( firstNe != null ) {
throw new DBDatasourceServiceException( firstNe );
}
throw new DBDatasourceServiceException( dsName );
} catch ( NamingException ne ) {
throw new DBDatasourceServiceException( ne );
}

// Always non-null.
throw firstNe;
}

/**
* Looks up a data source by name in a naming context.
*
* @param context The naming context.
* @param dsName The data source name.
* @param allowedJndiSchemes The list of allowed JNDI schemes of valid data source names.
* @return The data source resource; never {@code null}.
* @throws NamingException If the given data source name is not from a valid namespace, or if it does not exist
* in the given context.
* @throws ClassCastException If the given data source name resolves is that of a resource which does not implement
* {@link DataSource}.
*/
private static DataSource lookupJndiDataSourceByName( Context context,
String dsName,
List<String> allowedJndiSchemes ) throws NamingException {

// Validate dsName belongs to valid namespace.
validateJndiDataSourceName( dsName, allowedJndiSchemes );

DataSource ds = (DataSource) context.lookup( dsName );
if ( ds == null ) {
throw new NamingException(
Messages.getInstance().getErrorString( "PooledDatasourceHelper.ERROR_0010_DATASOURCE_NOT_FOUND", dsName ) );
}

return ds;
}

/**
* Validates a data source name.
*
* @param dsName The data source name.
* @param allowedJndiSchemes The list of allowed JNDI schemes of valid data source names.
* @throws NamingException If the data source name is invalid. Specifically, if it has a scheme which is not valid.
*/
private static void validateJndiDataSourceName( String dsName, List<String> allowedJndiSchemes )
throws NamingException {
if ( StringUtils.isEmpty( dsName ) ) {
return;
}

String scheme = getJndiScheme( dsName );
if ( scheme == null ) {
// Name with no URL scheme are assumed valid.
return;
}

if ( allowedJndiSchemes != null && !allowedJndiSchemes.contains( scheme ) ) {
throw new NamingException(
Messages.getInstance()
.getErrorString( "PooledDatasourceHelper.ERROR_0011_DATASOURCE_NAME_INVALID_SCHEME", dsName ) );
}
}

private static String getJndiScheme( String name ) {
int colonIndex = name.indexOf( ':' );
if ( colonIndex <= 0 ) {
return null;
}

int slashIndex = name.indexOf( '/' );
if ( slashIndex != -1 && colonIndex >= slashIndex ) {
return null;
}

return name.substring( 0, colonIndex );
}
// endregion
}
Loading