diff --git src/bp-core/bp-core-avatars.php src/bp-core/bp-core-avatars.php
index 44fe3c3..d7aa748 100644
--- src/bp-core/bp-core-avatars.php
+++ src/bp-core/bp-core-avatars.php
@@ -782,18 +782,84 @@ function bp_core_check_avatar_size( $file ) {
 }
 
 /**
+ * Get allowed avatar types/mimes
+ *
+ * @since BuddyPress (2.3.0)
+ *
+ * @param bool $mimes whether to get the mimes or extensions.
+ * @uses  apply_filters() call 'bp_core_get_allowed_avatar_type' to restrict the types
+ * @return array the list of allowed avatar types (mimes or extensions).
+ */
+function bp_core_get_allowed_avatar_types( $mimes = false ) {
+	$no_new_types = array(
+		'jpg'  => 0,
+		'gif'  => 1,
+		'png'  => 2,
+	);
+
+	/**
+	 * Use this filter to restrict image types
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @param array list of image types (eg: array( 'jpg', 'gif', 'png'));
+	 */
+	$avatar_types = (array) apply_filters( 'bp_core_get_allowed_avatar_types', array_keys( $no_new_types ) );
+
+	if ( empty( $avatar_types ) ) {
+		$allowed_types = $no_new_types;
+	} else {
+		$allowed_types = array_intersect_key( $no_new_types, array_flip( $avatar_types ) );
+	}
+
+	$avatar_types = array_flip( $allowed_types );
+
+	// If no mime is needed, return a list of allowed extensions
+	if ( empty( $mimes ) ) {
+		return $avatar_types;
+
+	// Return an array containing the list of allowed mimes
+	} else {
+		$allowed_mimes = $allowed_types;
+
+		// Transpose JPG to WordPress mime
+		if ( isset( $allowed_mimes['jpg'] ) ) {
+			$allowed_mimes['jpg|jpeg|jpe'] = $allowed_mimes['jpg'];
+		}
+
+		return array_intersect_key( array(
+			'jpg|jpeg|jpe' => 'image/jpeg',
+			'gif'          => 'image/gif',
+			'png'          => 'image/png',
+		), $allowed_mimes );
+	}
+}
+
+/**
  * Does the current avatar upload have an allowed file type?
  *
  * Permitted file types are JPG, GIF and PNG.
  *
  * @param array $file The $_FILES array.
+ * @uses bp_core_get_allowed_avatar_type()
+ * @uses wp_check_filetype()
  * @return bool True if the file extension is permitted, otherwise false.
  */
-function bp_core_check_avatar_type($file) {
-	if ( ( !empty( $file['file']['type'] ) && !preg_match('/(jpe?g|gif|png)$/i', $file['file']['type'] ) ) || !preg_match( '/(jpe?g|gif|png)$/i', $file['file']['name'] ) )
-		return false;
+function bp_core_check_avatar_type( $file ) {
+	$allowed_mimes = bp_core_get_allowed_avatar_types( true );
+
+	// Try with the file name
+	if ( ! empty( $file['file']['name'] ) ) {
+		$is_allowed = wp_check_filetype( $file['file']['name'], $allowed_mimes );
+		return ! empty( $is_allowed['ext'] );
+
+	// Try with the file type
+	} else if ( ! empty( $file['file']['type'] ) ) {
+		$is_allowed = array_flip( $allowed_mimes );
+		return isset( $is_allowed[ $file['file']['type'] ] );
+	}
 
-	return true;
+	return false;
 }
 
 /**
diff --git src/bp-core/classes/class-bp-attachment-avatar.php src/bp-core/classes/class-bp-attachment-avatar.php
index 8940954..074235b 100644
--- src/bp-core/classes/class-bp-attachment-avatar.php
+++ src/bp-core/classes/class-bp-attachment-avatar.php
@@ -35,13 +35,26 @@ class BP_Attachment_Avatar extends BP_Attachment {
 
 			// Specific errors for avatars
 			'upload_error_strings'  => array(
-				9  => sprintf( __( 'That photo is too big. Please upload one smaller than %s', 'buddypress' ), size_format( bp_core_avatar_original_max_filesize() ) ),
-				10 => __( 'Please upload only JPG, GIF or PNG photos.', 'buddypress' ),
+				9  => sprintf( __( 'That photo is too big. Please upload one smaller than %s.', 'buddypress' ), size_format( bp_core_avatar_original_max_filesize() ) ),
+				10 => sprintf( __( 'Please upload only these file types: %s.', 'buddypress' ), $this->get_avatar_types() ),
 			),
 		) );
 	}
 
 	/**
+	 * Gets the available avatar types
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @uses bp_core_get_allowed_avatar_types()
+	 * @return string comma separated list of allowed avatar types
+	 */
+	public function get_avatar_types() {
+		$types = array_map( 'strtoupper', bp_core_get_allowed_avatar_types() );
+		return join( ', ', $types );
+	}
+
+	/**
 	 * Set Upload Dir data for avatars
 	 *
 	 * @since BuddyPress (2.3.0)
diff --git src/bp-core/classes/class-bp-attachment.php src/bp-core/classes/class-bp-attachment.php
index 337c27f..e6a7bc4 100644
--- src/bp-core/classes/class-bp-attachment.php
+++ src/bp-core/classes/class-bp-attachment.php
@@ -16,7 +16,7 @@ defined( 'ABSPATH' ) || exit;
  *
  * @since BuddyPress (2.3.0)
  */
-class BP_Attachment {
+abstract class BP_Attachment {
 
 	/** Upload properties *****************************************************/
 
@@ -28,54 +28,20 @@ class BP_Attachment {
 	public $attachment = array();
 
 	/**
-	 * Maximum file size in kilobytes
-	 *
-	 * @var int
-	 */
-	public $original_max_filesize = 0;
-
-	/**
-	 * List of allowed file extensions
-	 * Defaults to get_allowed_mime_types()
-	 *
-	 * @var int
-	 */
-	public $allowed_mime_types = array();
-
-	/**
-	 * component's upload base directory.
-	 *
-	 * @var string
-	 */
-	public $base_dir = '';
-
-	/**
-	 * The upload action.
-	 *
-	 * @var string
-	 */
-	public $action = '';
-
-	/**
-	 * The file input name attribute
-	 *
-	 * @var string
-	 */
-	public $file_input = '';
-
-	/**
-	 * List of upload errors.
-	 *
-	 * @var array
-	 */
-	public $upload_error_strings = array();
-
-	/**
-	 * List of required core files
+	 * The default args to be merged with the
+	 * ones passed by the child class
 	 *
 	 * @var array
 	 */
-	public $required_wp_files = array( 'file' );
+	protected $default_args = array(
+		'original_max_filesize' => 0,
+		'allowed_mime_types'    => array(),
+		'base_dir'              => '',
+		'action'                => '',
+		'file_input'            => '',
+		'upload_error_strings'  => array(),
+		'required_wp_files'     => array( 'file' ),
+	);
 
 	/**
 	 * Construct Upload parameters
@@ -113,14 +79,20 @@ class BP_Attachment {
 		 * Max file size defaults to php ini settings or, in the case of
 		 * a multisite config, the root site fileupload_maxk option
 		 */
-		$this->original_max_filesize = (int) wp_max_upload_size();
+		$this->default_args['original_max_filesize'] = (int) wp_max_upload_size();
 
-		$params = bp_parse_args( $args, get_class_vars( __CLASS__ ), $this->action . '_upload_params' );
+		$params = bp_parse_args( $args, $this->default_args, $this->action . '_upload_params' );
 
 		foreach ( $params as $key => $param ) {
 			if ( 'upload_error_strings' === $key ) {
 				$this->{$key} = $this->set_upload_error_strings( $param );
-			} else {
+
+			// Sanitize the base dir
+			} else if ( 'base_dir' === $key ) {
+				$this->{$key} = sanitize_title( $param );
+
+			// Action & File input are already set and sanitized
+			} else if ( 'action' != $key && 'file_input' != $key ) {
 				$this->{$key} = $param;
 			}
 		}
@@ -395,7 +367,7 @@ class BP_Attachment {
 		}
 
 		// Check if upload path already exists
-		if ( ! file_exists( $this->upload_path ) ) {
+		if ( ! is_dir( $this->upload_path ) ) {
 
 			// If path does not exist, attempt to create it
 			if ( ! wp_mkdir_p( $this->upload_path ) ) {
@@ -429,7 +401,7 @@ class BP_Attachment {
 	public function crop( $args = array() ) {
 		$wp_error = new WP_Error();
 
-		$r = wp_parse_args( $args, array(
+		$r = bp_parse_args( $args, array(
 			'original_file' => '',
 			'crop_x'        => 0,
 			'crop_y'        => 0,
@@ -439,7 +411,7 @@ class BP_Attachment {
 			'dst_h'         => 0,
 			'src_abs'       => false,
 			'dst_file'      => false,
-		) );
+		), 'bp_attachment_crop_args' );
 
 		if ( empty( $r['original_file'] ) || ! file_exists( $r['original_file'] ) ) {
 			$wp_error->add( 'crop_error', __( 'Cropping the file failed: missing source file.', 'buddypress' ) );
diff --git tests/phpunit/assets/attachment-extensions.php tests/phpunit/assets/attachment-extensions.php
index e69de29..117b456 100644
--- tests/phpunit/assets/attachment-extensions.php
+++ tests/phpunit/assets/attachment-extensions.php
@@ -0,0 +1,10 @@
+<?php
+/**
+ * The following implementations of BP_Attachment act as dummy plugins
+ * for our unit tests
+ */
+class BP_Attachment_Extend extends BP_Attachment {
+	public function __construct( $args = array() ) {
+		return parent::__construct( $args );
+	}
+}
diff --git tests/phpunit/testcases/core/class-bp-attachment-avatar.php tests/phpunit/testcases/core/class-bp-attachment-avatar.php
index e69de29..c95921c 100644
--- tests/phpunit/testcases/core/class-bp-attachment-avatar.php
+++ tests/phpunit/testcases/core/class-bp-attachment-avatar.php
@@ -0,0 +1,250 @@
+<?php
+
+/**
+ * @group core
+ * @group avatars
+ * @group bp_attachments
+ * @group BP_Attachement_Avatar
+ */
+class BP_Tests_BP_Attachement_Avatar_TestCases extends BP_UnitTestCase {
+	protected $displayed_user;
+
+	public function setUp() {
+		parent::setUp();
+		$bp = buddypress();
+		$this->displayed_user = $bp->displayed_user;
+		$bp->displayed_user = new stdClass;
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		buddypress()->displayed_user = $this->displayed_user;
+	}
+
+	public function max_filesize() {
+		return 1000;
+	}
+
+	private function clean_avatars( $type = 'user' ) {
+		if ( 'user' === $type ) {
+			$avatar_dir = 'avatars';
+		} elseif ( 'group' === $type ) {
+			$avatar_dir = 'group-avatars';
+		}
+
+		$this->rrmdir( bp_core_avatar_upload_path() . '/' . $avatar_dir );
+	}
+
+	private function rrmdir( $dir ) {
+		$d = glob( $dir . '/*' );
+
+		if ( empty( $d ) ) {
+			return;
+		}
+
+		foreach ( $d as $file ) {
+			if ( is_dir( $file ) ) {
+				$this->rrmdir( $file );
+			} else {
+				@unlink( $file );
+			}
+		}
+
+		@rmdir( $dir );
+	}
+
+	/**
+	 * @group upload
+	 */
+	public function test_upload_user_avatar_no_error() {
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+
+		$u1 = $this->factory->user->create();
+		buddypress()->displayed_user->id = $u1;
+
+		$avatar = BP_TESTS_DIR . 'assets/files/disc.png';
+
+		$tmp_name = wp_tempnam( $avatar );
+
+		copy( $avatar, $tmp_name );
+
+		// Upload the file
+		$avatar_attachment = new BP_Attachment_Avatar();
+		$_POST['action'] = $avatar_attachment->action;
+		$_FILES[ $avatar_attachment->file_input ] = array(
+			'tmp_name' => $tmp_name,
+			'name'     => 'disc.png',
+			'type'     => 'image/png',
+			'error'    => 0,
+			'size'     => filesize( $avatar )
+		);
+
+		$user_avatar = $avatar_attachment->upload( $_FILES, 'xprofile_avatar_upload_dir' );
+
+		$this->assertTrue( empty( $user_avatar['error'] ) );
+
+		// clean up!
+		$this->clean_avatars();
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+	}
+
+	/**
+	 * @group upload
+	 */
+	public function test_upload_group_avatar_no_error() {
+		$bp = buddypress();
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+		$reset_current_group = $bp->groups->current_group;
+
+		$g = $this->factory->group->create();
+
+		$bp->groups->current_group = groups_get_group( array(
+			'group_id'        => $g,
+			'populate_extras' => true,
+		) );
+
+		$avatar = BP_TESTS_DIR . 'assets/files/disc.png';
+
+		$tmp_name = wp_tempnam( $avatar );
+
+		copy( $avatar, $tmp_name );
+
+		// Upload the file
+		$avatar_attachment = new BP_Attachment_Avatar();
+		$_POST['action'] = $avatar_attachment->action;
+		$_FILES[ $avatar_attachment->file_input ] = array(
+			'tmp_name' => $tmp_name,
+			'name'     => 'disc.png',
+			'type'     => 'image/png',
+			'error'    => 0,
+			'size'     => filesize( $avatar )
+		);
+
+		$group_avatar = $avatar_attachment->upload( $_FILES, 'groups_avatar_upload_dir' );
+
+		$this->assertTrue( empty( $group_avatar['error'] ) );
+
+		// clean up!
+		$this->clean_avatars( 'group' );
+		$bp->groups->current_group = $reset_current_group;
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+	}
+
+	/**
+	 * @group upload
+	 */
+	public function test_upload_user_avatar_file_size_error() {
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+
+		$u1 = $this->factory->user->create();
+		buddypress()->displayed_user->id = $u1;
+
+		$avatar = BP_TESTS_DIR . 'assets/files/disc.png';
+
+		$tmp_name = wp_tempnam( $avatar );
+
+		copy( $avatar, $tmp_name );
+
+		add_filter( 'bp_core_avatar_original_max_filesize', array( $this, 'max_filesize' ) );
+
+		// Upload the file
+		$avatar_attachment = new BP_Attachment_Avatar();
+
+		$_POST['action'] = $avatar_attachment->action;
+		$_FILES[ $avatar_attachment->file_input ] = array(
+			'tmp_name' => $tmp_name,
+			'name'     => 'disc.png',
+			'type'     => 'image/png',
+			'error'    => 0,
+			'size'     => filesize( $avatar )
+		);
+
+		$user_avatar = $avatar_attachment->upload( $_FILES, 'xprofile_avatar_upload_dir' );
+
+		remove_filter( 'bp_core_avatar_original_max_filesize', array( $this, 'max_filesize' ) );
+
+		$this->assertFalse( empty( $user_avatar['error'] ) );
+
+		// clean up!
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+	}
+
+	/**
+	 * @group upload
+	 */
+	public function test_upload_user_avatar_file_type_error() {
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+
+		$u1 = $this->factory->user->create();
+		buddypress()->displayed_user->id = $u1;
+
+		$avatar = BP_TESTS_DIR . 'assets/files/buddypress_logo.pdf';
+
+		$tmp_name = wp_tempnam( $avatar );
+
+		copy( $avatar, $tmp_name );
+
+		// Upload the file
+		$avatar_attachment = new BP_Attachment_Avatar();
+		$_POST['action'] = $avatar_attachment->action;
+		$_FILES[ $avatar_attachment->file_input ] = array(
+			'tmp_name' => $tmp_name,
+			'name'     => 'buddypress_logo.pdf',
+			'type'     => 'application/pdf',
+			'error'    => 0,
+			'size'     => filesize( $avatar )
+		);
+
+		$user_avatar = $avatar_attachment->upload( $_FILES, 'xprofile_avatar_upload_dir' );
+
+		$this->assertFalse( empty( $user_avatar['error'] ) );
+
+		// clean up!
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+	}
+
+	/**
+	 * @group crop
+	 */
+	public function test_crop_avatar() {
+		$avatar = BP_TESTS_DIR . 'assets/files/logo.jpg';
+		$pdf    = BP_TESTS_DIR . 'assets/files/buddypress_logo.pdf';
+
+		$u1 = $this->factory->user->create();
+
+		$upload_dir = xprofile_avatar_upload_dir( 'avatars', $u1 );
+
+		wp_mkdir_p( $upload_dir['path'] );
+
+		copy( $avatar, $upload_dir['path'] . '/logo.jpg' );
+		copy( $pdf, $upload_dir['path'] . '/buddypress_logo.pdf' );
+
+		$crop_args = array(
+			'object'        => 'user',
+			'avatar_dir'    => 'avatars',
+			'item_id'       => $u1,
+			'original_file' => '/avatars/' . $u1 . '/logo.jpg',
+		);
+
+		$avatar_attachment = new BP_Attachment_Avatar();
+		$cropped = $avatar_attachment->crop( $crop_args );
+
+		$this->assertTrue( ! empty( $cropped['full'] ) && ! is_wp_error( $cropped['full'] ) );
+
+		$crop_args['original_file'] = '/avatars/' . $u1 . '/buddypress_logo.pdf';
+
+		$cropped = $avatar_attachment->crop( $crop_args );
+		$this->assertTrue( is_wp_error( $cropped['full'] ) );
+
+		// Clean up
+		$this->clean_avatars();
+	}
+}
diff --git tests/phpunit/testcases/core/class-bp-attachment.php tests/phpunit/testcases/core/class-bp-attachment.php
index e69de29..f0cc053 100644
--- tests/phpunit/testcases/core/class-bp-attachment.php
+++ tests/phpunit/testcases/core/class-bp-attachment.php
@@ -0,0 +1,175 @@
+<?php
+
+include_once BP_TESTS_DIR . '/assets/attachment-extensions.php';
+
+/**
+ * @group bp_attachments
+ * @group BP_Attachement
+ */
+class BP_Tests_BP_Attachment_TestCases extends BP_UnitTestCase {
+
+	private function clean_files( $basedir = 'attachment_base_dir' ) {
+		$upload_dir = bp_upload_dir();
+
+		$this->rrmdir( $upload_dir['basedir'] . '/' . $basedir );
+	}
+
+	private function rrmdir( $dir ) {
+		$d = glob( $dir . '/*' );
+
+		if ( empty( $d ) ) {
+			return;
+		}
+
+		foreach ( $d as $file ) {
+			if ( is_dir( $file ) ) {
+				$this->rrmdir( $file );
+			} else {
+				@unlink( $file );
+			}
+		}
+
+		@rmdir( $dir );
+	}
+
+	public function test_construct_missing_required_parameter() {
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+
+		$_FILES['file'] = array(
+			'name'     => 'disc.png',
+			'type'     => 'image/png',
+			'error'    => 0,
+			'size'     => 10000
+		);
+
+		$attachment_class = new BP_Attachment_Extend();
+		$upload = $attachment_class->upload( $_FILES );
+
+		$this->assertTrue( empty( $upload ) );
+
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+	}
+
+	public function test_set_upload_dir() {
+		$upload_dir = bp_upload_dir();
+
+		$attachment_class = new BP_Attachment_Extend( array(
+			'action'     => 'attachment_action',
+			'file_input' => 'attachment_file_input'
+		) );
+
+		$this->assertSame( $attachment_class->upload_dir, bp_upload_dir() );
+
+		$attachment_class = new BP_Attachment_Extend( array(
+			'action'     => 'attachment_action',
+			'file_input' => 'attachment_file_input',
+			'base_dir'   => 'attachment_base_dir',
+		) );
+
+		$this->assertTrue( file_exists( $upload_dir['basedir'] . '/attachment_base_dir'  ) );
+
+		// clean up
+		$this->clean_files();
+	}
+
+	/**
+	 * @group upload
+	 */
+	public function test_upload() {
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+
+		$file = BP_TESTS_DIR . 'assets/files/disc.png';
+
+		$tmp_name = wp_tempnam( $file );
+
+		copy( $file, $tmp_name );
+
+		$attachment_class = new BP_Attachment_Extend( array(
+			'action'                => 'attachment_action',
+			'file_input'            => 'attachment_file_input',
+			'base_dir'   		    => 'attachment_base_dir',
+			'original_max_filesize' => 10000,
+		) );
+
+		$_POST['action'] = $attachment_class->action;
+		$_FILES[ $attachment_class->file_input ] = array(
+			'tmp_name' => $tmp_name,
+			'name'     => 'disc.png',
+			'type'     => 'image/png',
+			'error'    => 0,
+			'size'     => filesize( $file )
+		);
+
+		// Error: file size
+		$upload = $attachment_class->upload( $_FILES );
+		$this->assertFalse( empty( $upload['error'] ) );
+
+		$attachment_class->allowed_mime_types    = array( 'pdf' );
+		$attachment_class->original_max_filesize = false;
+
+		// Error: file type
+		$upload = $attachment_class->upload( $_FILES );
+		$this->assertFalse( empty( $upload['error'] ) );
+
+		$attachment_class->allowed_mime_types = array();
+
+		// Success
+		$upload = $attachment_class->upload( $_FILES );
+		$this->assertTrue( empty( $upload['error'] ) );
+
+		// clean up!
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+		$this->clean_files();
+	}
+
+	/**
+	 * @group crop
+	 */
+	public function test_crop_image() {
+		$image = BP_TESTS_DIR . 'assets/files/logo.jpg';
+
+		$crop_args = array(
+			'original_file' => $image,
+			'crop_x'        => 0,
+			'crop_y'        => 0,
+			'crop_w'        => 150,
+			'crop_h'        => 150,
+			'dst_w'         => 150,
+			'dst_h'         => 150,
+		);
+
+		$attachment_class = new BP_Attachment_Extend( array(
+			'action'                => 'attachment_action',
+			'file_input'            => 'attachment_file_input',
+			'base_dir'   		    => 'attachment_base_dir',
+		) );
+		$cropped = $attachment_class->crop( $crop_args );
+
+		// Image must come from the upload basedir
+		$this->assertTrue( is_wp_error( $cropped ) );
+
+		$crop_args['original_file'] = $attachment_class->upload_path . '/logo.jpg';
+
+		// Move to the base upload dir
+		copy( $image, $crop_args['original_file'] );
+
+		// Image must stay in the upload basedir
+		$crop_args['dst_file'] = BP_TESTS_DIR . 'assets/files/error.jpg';
+		$cropped = $attachment_class->crop( $crop_args );
+
+		// Image must stay in the upload basedir
+		$this->assertTrue( is_wp_error( $cropped ) );
+
+		unset( $crop_args['dst_file'] );
+
+		$cropped = $attachment_class->crop( $crop_args );
+		$this->assertFalse( is_wp_error( $cropped ) );
+
+		// clean up!
+		$this->clean_files();
+	}
+}
diff --git tests/phpunit/testcases/core/functions.php tests/phpunit/testcases/core/functions.php
index 1ab3b45..d165739 100644
--- tests/phpunit/testcases/core/functions.php
+++ tests/phpunit/testcases/core/functions.php
@@ -572,4 +572,26 @@ class BP_Tests_Core_Functions extends BP_UnitTestCase {
 			date_default_timezone_set( $tz_backup );
 		}
 	}
+
+	/**
+	 * @group bp_attachments
+	 * @group bp_upload_dir
+	 */
+	public function test_bp_upload_dir_ms() {
+		if ( ! is_multisite() ) {
+			return;
+		}
+
+		$expected_upload_dir = wp_upload_dir();
+
+		$b = $this->factory->blog->create();
+
+		switch_to_blog( $b );
+
+		$tested_upload_dir = bp_upload_dir();
+
+		restore_current_blog();
+
+		$this->assertSame( $expected_upload_dir, $tested_upload_dir );
+	}
 }
