Hardening BuddyPress Group Documents

The BuddyPress Group Documents plugin allows groups a handy way for users to share documents with fellow members of a BP group. It’s crucial to the work that is done on the CUNY Academic Commons. But, by default, the plugin stores documents in a subdirectory of your WP uploads folder (usually /wp-content/blogs.dir/ on multisite). That means that documents are available directly, to anyone who has the URL, regardless of the public/private/hidden status of groups. This isn’t a problem from within BuddyPress, since URLs for the documents only appear inside of the protected group interface. But if the URL is shared, then the document becomes publicly available. Worse, if someone posts the URL of a document in a public place, search engine bots will find it, and the contents of the document could end up in Google.

I wrote a few helper functions to change this behavior. The strategy is this: Move the files so that they are not accessible via URL, ie in a directory above the web root. (In my case, it’s a directory called bp-group-documents, just above my web root.) Then, catch requests of a certain type (I’ve chosen to go with a URL parameter get_group_doc=), and check them to see whether the current user has the adequate permission to access the document in question. Finally, make sure that all of the URLs and paths that BPGD uses to upload and display documents are filtered to the updated versions. I’ve provided my code below – use and modify at your pleasure. You should be able to place it in your plugins/bp-custom.php file, and then move your existing docs from their current location (probably something like wp-content/blogs.dir/1/files/group-documents) to the new directory.

I also added a line to my .htaccess file to ensure that requests to the old URLs are redirected to the new, hardened URL. That line is this:


RewriteRule ^wp\-content/blogs\.dir/1/files/group\-documents/(.*) /?get_group_doc=$1 [R,L]
 

Obviously, you may have to modify it for different file paths.

EDITED Feb 8, 2011 to include the code for creating directories when none exist


define( 'BP_GROUP_DOCUMENTS_SECURE_PATH', substr( ABSPATH, 0, strrpos( rtrim( ABSPATH, '/' ), '/' ) ) . '/bp-group-documents/' );

function cac_filter_doc_url( $doc_url, $group_id, $file ) {
	$url = bp_get_root_domain() . '?get_group_doc=' . $group_id . '/' . $file;
	return $url;
}
add_filter( 'bp_group_documents_file_url', 'cac_filter_doc_url', 10, 3 );

function cac_filter_doc_path( $doc_url, $group_id, $file ) {
	$document_dir = BP_GROUP_DOCUMENTS_SECURE_PATH . $group_id . '/';
	
	if ( !is_dir( $document_dir ) )
		mkdir( $document_dir, 0775, true );

	$path = BP_GROUP_DOCUMENTS_SECURE_PATH . $group_id . '/' . $file;
	return $path;
}
add_filter( 'bp_group_documents_file_path', 'cac_filter_doc_path', 10, 3 );

function cac_catch_group_doc_request() {
	$error = false;

	if ( empty( $_GET['get_group_doc'] ) )
		return;
	
	$doc_id = $_GET['get_group_doc'];
	
	// Check to see whether the current user has access to the doc in question
	$file_deets 	= explode( '/', $doc_id );
	$group_id 	= $file_deets[0];	
	$group		= new BP_Groups_Group( $group_id );
	
	if ( empty( $group->id ) ) {
		$error = array(
			'message' 	=> 'That group does not exist.',
			'redirect'	=> bp_get_root_domain()
		);
	} else {
		if ( $group->status != 'public' ) {
			// If the group is not public, then the user must be logged in and
			// a member of the group to download the document
			if ( !is_user_logged_in() || !groups_is_user_member( bp_loggedin_user_id(), $group_id ) ) {
				$error = array(
					'message' 	=> sprintf( 'You must be a logged-in member of the group %s to access this document. If you are a member of the group, please log into the site and try again.', $group->name ),
					'redirect'	=> bp_get_group_permalink( $group )
				);
			}
		}
		
		// If we have gotten this far without an error, then the download can go through
		if ( !$error ) {
			
			$doc_path = BP_GROUP_DOCUMENTS_SECURE_PATH . $doc_id;
			
			if ( file_exists( $doc_path ) ) {
				$mime_type = mime_content_type( $doc_path );
				$doc_size = filesize( $doc_path );
				
				header("Cache-Control: public, must-revalidate, post-check=0, pre-check=0");
				header("Pragma: hack");
					
				header("Content-Type: $mime_type; name='" . $file_deets[1] . "'");
				header("Content-Length: " . $doc_size );
				
				header('Content-Disposition: attachment; filename="' . $file_deets[1] . '"');
				header("Content-Transfer-Encoding: binary");              
				ob_clean();
				flush();  		
				readfile( $doc_path );
				die();
		       
			} else {
				// File does not exist
				$error = array(
					'message' 	=> 'The file could not be found.',
					'redirect'	=> bp_get_group_permalink( $group ) . '/documents'
				);
			}
		}
	}
		
	// If we have gotten this far, there was an error. Add a message and redirect
	bp_core_add_message( $error['message'], 'error' );
	bp_core_redirect( $error['redirect'] );
}
add_filter( 'wp', 'cac_catch_group_doc_request', 1 );

// http://www.php.net/manual/en/function.mime-content-type.php#87856
if(!function_exists('mime_content_type')) {

    function mime_content_type($filename) {

        $mime_types = array(

            'txt' => 'text/plain',
            'htm' => 'text/html',
            'html' => 'text/html',
            'php' => 'text/html',
            'css' => 'text/css',
            'js' => 'application/javascript',
            'json' => 'application/json',
            'xml' => 'application/xml',
            'swf' => 'application/x-shockwave-flash',
            'flv' => 'video/x-flv',

            // images
            'png' => 'image/png',
            'jpe' => 'image/jpeg',
            'jpeg' => 'image/jpeg',
            'jpg' => 'image/jpeg',
            'gif' => 'image/gif',
            'bmp' => 'image/bmp',
            'ico' => 'image/vnd.microsoft.icon',
            'tiff' => 'image/tiff',
            'tif' => 'image/tiff',
            'svg' => 'image/svg+xml',
            'svgz' => 'image/svg+xml',

            // archives
            'zip' => 'application/zip',
            'rar' => 'application/x-rar-compressed',
            'exe' => 'application/x-msdownload',
            'msi' => 'application/x-msdownload',
            'cab' => 'application/vnd.ms-cab-compressed',

            // audio/video
            'mp3' => 'audio/mpeg',
            'qt' => 'video/quicktime',
            'mov' => 'video/quicktime',

            // adobe
            'pdf' => 'application/pdf',
            'psd' => 'image/vnd.adobe.photoshop',
            'ai' => 'application/postscript',
            'eps' => 'application/postscript',
            'ps' => 'application/postscript',

            // ms office
            'doc' => 'application/msword',
            'rtf' => 'application/rtf',
            'xls' => 'application/vnd.ms-excel',
            'ppt' => 'application/vnd.ms-powerpoint',

            // open office
            'odt' => 'application/vnd.oasis.opendocument.text',
            'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
        );

        $ext = strtolower(array_pop(explode('.',$filename)));
        if (array_key_exists($ext, $mime_types)) {
            return $mime_types[$ext];
        }
        elseif (function_exists('finfo_open')) {
            $finfo = finfo_open(FILEINFO_MIME);
            $mimetype = finfo_file($finfo, $filename);
            finfo_close($finfo);
            return $mimetype;
        }
        else {
            return 'application/octet-stream';
        }
    }
}


 

12 thoughts on “Hardening BuddyPress Group Documents”

  1. Have just started using BP Group Documents and would love to use you hardening code to make files more protected. Unfortunately I don’t know quite how to modify it to suit. I’ve added the code above to plugins/bp-customs.php, but I’m having trouble with the path:

    Warning: move_uploaded_file(/var/www/htdocs/my-user/domains/site-folder/bp-group-documents/3/1322001518-img001.jpg

    The “site-folder” is my site root and I’ve placed the bp-group-documents folder above it, at:

    /var/www/htdocs/my-user/domains/bp-group-documents

    But I don’t know how to change your code so that it looks for the bp-group-documents folder one step above the site root folder? Any hints would be much appreciated!

  2. Your code example was just what I was looking for. It allows the bp-group-documents folder that is used to store group files to be be placed outside your DocumentRoot to prevent direct linking and search engine crawlers that do not comply to denial in robots.txt. And it adds an extra security layer that filters each file download request throught BP to check if the visitor is logged in and has access to the group. Great!

  3. Hi does anyone has the original (latest) plugin file to download? it is know longer available to download over by buddypress

  4. Hi, this looks like a great piece of code, thanks for sharing. I’m hoping for a little advice… I’m not actually using the BP Group Documents plugin, but I’m using a couple of others that allow general media uploads within the group. I’d like to harden the path to any media uploads to private groups. Could this code be adjusted to do this? Thanks so much.

  5. Haylc – Theoretically, yes. The key thing is that you’ll have to be able to filter (a) the location where the plugins store the files, and (b) the URL that is output when rendering the page. Beyond that, you should mostly be able to use my code as is.

Leave a Reply

Your email address will not be published. Required fields are marked *

*
To prove you're a person (not a spam script), type the security word shown in the picture. Click on the picture to hear an audio file of the word.
Anti-spam image