Skip to content
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
9 changes: 5 additions & 4 deletions app/helper/RTMediaModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public function get( $columns, $offset = false, $per_page = false, $order_by = '
} else {
$join .= " LEFT JOIN {$wpdb->prefix}{$this->meta_table_name} as {$tbl_alias} ON {$this->table_name}.id = {$tbl_alias}.media_id ";
}
$meta_query['compare'] = esc_sql( $meta_query['compare'] );
$meta_query['compare'] = $this->sanitize_sql_compare_operator( $meta_query['compare'], '=' );
if ( isset( $meta_query['value'] ) ) {
$where .= $wpdb->prepare( " AND ({$tbl_alias}.meta_key = %s and {$tbl_alias}.meta_value {$meta_query["compare"]} %s ) ", $meta_query['key'], $meta_query['value'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
} else {
Expand All @@ -105,14 +105,15 @@ public function get( $columns, $offset = false, $per_page = false, $order_by = '
$compare = $colvalue['compare'];
}

$compare = $this->sanitize_sql_compare_operator( $compare );

$tmp_val = isset( $colvalue['value'] ) ? $colvalue['value'] : $colvalue;
$col_val_comapare = ( is_array( $tmp_val ) ) ? implode( "','", esc_sql( $tmp_val ) ) : esc_sql( $tmp_val );

if ( 'IS NOT' === $compare ) {
$col_val_comapare = ! empty( $colvalue['value'] ) ? $colvalue['value'] : $col_val_comapare;
$col_val_comapare = ! empty( $colvalue['value'] ) ? esc_sql( $colvalue['value'] ) : $col_val_comapare;
}

$compare = esc_sql( $compare );
$where .= " AND {$this->table_name}.{$colname} {$compare} ('{$col_val_comapare}')";
} else {
$where .= $wpdb->prepare( " AND {$this->table_name}.{$colname} = %s", $colvalue ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
Expand Down Expand Up @@ -404,10 +405,10 @@ public function get_counts( $user_id = false, $where_query = false ) {
} else {
$compare = $colvalue['compare'];
}
$compare = $this->sanitize_sql_compare_operator( $compare );
if ( ! isset( $colvalue['value'] ) ) {
$colvalue['value'] = $colvalue;
}
$compare = esc_sql( $compare );
$where_query_sql .= " AND {$this->table_name}.{$colname} {$compare} ('" . implode( "','", esc_sql( $colvalue['value'] ) ) . "')";
} else {
$where_query_sql .= $wpdb->prepare( " AND {$this->table_name}.{$colname} = %s", $colvalue ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
Expand Down
23 changes: 21 additions & 2 deletions app/helper/db/RTDBModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,24 @@ public function update( $data, $where ) {
return $wpdb->update( $this->table_name, $data, $where ); // phpcs:ignore
}

/**
* Validate a SQL comparison operator against an allowlist.
*
* The operator is interpolated directly into query strings, so it must be
* restricted to a fixed set of known-safe operators to prevent SQL injection.
*
* @param string $compare Operator supplied by the caller.
* @param string $default Fallback operator when $compare is not allowed.
*
* @return string Safe SQL comparison operator.
*/
public function sanitize_sql_compare_operator( $compare, $default = 'IN' ) {
$allowed = array( '=', '!=', '<>', '>', '>=', '<', '<=', 'IN', 'NOT IN', 'LIKE', 'IS', 'IS NOT' );
$compare = strtoupper( trim( (string) $compare ) );

return in_array( $compare, $allowed, true ) ? $compare : $default;
}

/**
* Get all the rows according to the columns set in $columns parameter.
* offset and rows per page can also be passed for pagination.
Expand All @@ -206,10 +224,11 @@ public function get( $columns, $offset = false, $per_page = false, $order_by = '
} else {
$compare = $colvalue['compare'];
}
$compare = $this->sanitize_sql_compare_operator( $compare );
if ( ! isset( $colvalue['value'] ) ) {
$colvalue['value'] = esc_sql( $colvalue );
$colvalue['value'] = $colvalue;
}
$col_val_comapare = ( is_array( $colvalue['value'] ) ) ? '(\'' . implode( "','", $colvalue['value'] ) . '\')' : '(\'' . $colvalue['value'] . '\')';
$col_val_comapare = ( is_array( $colvalue['value'] ) ) ? '(\'' . implode( "','", esc_sql( $colvalue['value'] ) ) . '\')' : '(\'' . esc_sql( $colvalue['value'] ) . '\')';
$where .= " AND {$this->table_name}.{$colname} {$compare} {$col_val_comapare}";
} else {
$where .= $wpdb->prepare( " AND {$this->table_name}.{$colname} = %s", $colvalue ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
Expand Down
72 changes: 72 additions & 0 deletions app/main/controllers/api/RTMediaJsonApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,18 @@ public function rtmedia_api_process_rtmedia_upload_media_request() {
$uploaded['description'] = $description;
$uploaded['taxonomy'] = array();
$uploaded['custom_fields'] = array();

// Verify the token user may write to the requested album/context before adding.
$upload_model = new RTMediaUploadModel();
$upload_model->upload = array_merge( $upload_model->upload, $uploaded );
$upload_model->authorize_targets();
$uploaded['album_id'] = $upload_model->upload['album_id'];
$uploaded['context'] = $upload_model->upload['context'];
$uploaded['context_id'] = $upload_model->upload['context_id'];
if ( ! in_array( intval( $uploaded['privacy'] ), array( 0, 20, 40, 60 ), true ) ) {
$uploaded['privacy'] = get_rtmedia_default_privacy();
}

$rtmedia = new RTMediaMedia();
$rtupload = $rtmedia->add( $uploaded, $new_look );
$id = rtmedia_media_id( $rtupload[0] );
Expand Down Expand Up @@ -1447,18 +1459,78 @@ public function rtmedia_api_process_rtmedia_get_media_details_request() {
);
$media = $rtmediamodel->get( $args );
}

if ( ! empty( $media ) && ! $this->rtmedia_api_current_user_can_view_media( $media[0] ) ) {
wp_send_json( $this->rtmedia_api_response_object( 'FALSE', $this->ec_invalid_media_id, $this->msg_invalid_media_id ) );
}

$activity_id = ! empty( $media ) ? $media[0]->activity_id : '';

if ( empty( $activity_id ) ) {
wp_send_json( $this->rtmedia_api_response_object( 'FALSE', $this->ec_invalid_media_id, $this->msg_invalid_media_id ) );
}
$media_single = $this->rtmediajsonapifunction->rtmedia_api_get_feed( false, $activity_id );

// This endpoint returns a single media item; drop any sibling media sharing the
// same activity so another user's media is never exposed alongside the requested one.
if ( ! empty( $media_single[0]['media'] ) && is_array( $media_single[0]['media'] ) ) {
$requested_media = array();
foreach ( $media_single[0]['media'] as $item ) {
if ( isset( $item['id'] ) && (int) $item['id'] === (int) $media_id ) {
$requested_media[] = $item;
}
}
$media_single[0]['media'] = $requested_media;

// Re-scope comments to the requested media; get_feed sets them from the
// activity's first media, which may belong to another user.
if ( isset( $media_single[0]['comments'] ) ) {
$media_single[0]['comments'] = $this->rtmediajsonapifunction->rtmedia_api_get_media_comments( $media_id );
}
}

if ( $media_single ) {
wp_send_json( $this->rtmedia_api_response_object( 'TRUE', $ec_single_media, $msg_single_media, $media_single ) );
}
}

/**
* Check whether the token-authenticated API user may view a media item.
*
* Mirrors RTMediaQuery::privacy_filter(), which is not registered during the
* API request lifecycle, so private media must be gated explicitly here.
*
* @param object $media Media row returned by RTMediaModel::get().
*
* @return bool True if the current API user may view the media.
*/
private function rtmedia_api_current_user_can_view_media( $media ) {
$privacy = isset( $media->privacy ) && null !== $media->privacy ? (int) $media->privacy : 0;
$author = isset( $media->media_author ) ? (int) $media->media_author : 0;
$user = (int) $this->user_id;

if ( $user && user_can( $user, 'list_users' ) ) {
return true;
}
// Public is exactly NULL or 0; values such as -1 / 80 are blocked, not public.
if ( 0 === $privacy ) {
return true;
}
if ( $user ) {
if ( 20 === $privacy ) {
return true;
}
if ( $author === $user && $privacy >= 40 ) {
return true;
}
if ( 40 === $privacy && function_exists( 'friends_check_friendship' ) && friends_check_friendship( $author, $user ) ) {
return true;
}
}

return false;
}

/**
* Function to log out user from api.
*/
Expand Down
5 changes: 3 additions & 2 deletions app/main/controllers/api/RTMediaJsonApiFunctions.php
Original file line number Diff line number Diff line change
Expand Up @@ -348,13 +348,14 @@ public function rtmedia_api_get_media_comments( $media_id ) {

$comments = get_comments(
array(
'comment_post_ID' => $id,
'number' => 100,
'post_id' => $id,
'number' => 100,
)
);

$media_comments = array();
if ( ! empty( $comments ) ) {
$media_comments['user'] = array();

foreach ( $comments as $comment ) {
$media_comments['comments'][] = array(
Expand Down
5 changes: 5 additions & 0 deletions app/main/controllers/upload/RTMediaUploadEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ public function template_redirect( $create_activity = true ) {
} // End if.

$this->upload = apply_filters( 'rtmedia_media_param_before_upload', $this->upload );

$model->upload = $this->upload;
$model->authorize_targets();
$this->upload = $model->upload;

$rtupload = new RTMediaUpload( $this->upload );

if ( $comment_media ) {
Expand Down
109 changes: 99 additions & 10 deletions app/main/controllers/upload/RTMediaUploadModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ public function has_context() {
*/
public function sanitize_object() {

// Never trust a client-supplied author: force the acting user. On the token API
// (no WP session) keep the author the API layer has already set.
if ( is_user_logged_in() ) {
$this->upload['media_author'] = get_current_user_id();
} elseif ( ! $this->has_author() ) {
$this->set_author();
}

if ( ! $this->has_context() ) {
// Set context_id to Logged in user id if context is profile and context_id is not provided.
if ( 'profile' === $this->upload['context'] || 'bp_member' === $this->upload['context'] ) {
Expand All @@ -79,6 +87,10 @@ public function sanitize_object() {
}
}

// Verify the acting user may write to the requested context and album; otherwise
// fall back to safe defaults (own profile / own default album).
$this->authorize_targets();

if ( ! is_array( $this->upload['taxonomy'] ) ) {
$this->upload['taxonomy'] = array( $this->upload['taxonomy'] );
}
Expand All @@ -87,14 +99,6 @@ public function sanitize_object() {
$this->upload['custom_fields'] = array( $this->upload['custom_fields'] );
}

if ( ! $this->has_album_id() || ! $this->has_album_permissions() ) {
$this->set_album_id();
}

if ( ! $this->has_author() ) {
$this->set_author();
}

if ( is_rtmedia_privacy_enable() ) {

if ( is_rtmedia_privacy_user_overide() ) {
Expand All @@ -112,6 +116,11 @@ public function sanitize_object() {
} else {
$this->upload['privacy'] = 0;
}

// Restrict privacy to the known set; -1/80 are moderation states, not user-settable.
if ( ! in_array( intval( $this->upload['privacy'] ), array( 0, 20, 40, 60 ), true ) ) {
$this->upload['privacy'] = get_rtmedia_default_privacy();
}
}

/**
Expand Down Expand Up @@ -149,8 +158,88 @@ public function has_album_id() {
* @return boolean
*/
public function has_album_permissions() {
// yet to be coded for the privacy options of the album.
return true;
$album_id = $this->upload['album_id'];
if ( ! $album_id || 'undefined' === $album_id ) {
return false;
}

$user = intval( $this->upload['media_author'] );

// Site admins bypass, matching rtMedia's is_rt_admin() ( list_users ) convention.
if ( $user && user_can( $user, 'list_users' ) ) {
return true;
}

// The shared global "wall post" albums are a valid destination for everyone.
if ( function_exists( 'rtmedia_global_albums' ) ) {
$globals = array_map( 'intval', (array) rtmedia_global_albums() );
if ( in_array( intval( $album_id ), $globals, true ) ) {
return true;
}
}

$model = new RTMediaModel();
$album = $model->get( array( 'id' => $album_id ) );
if ( empty( $album ) ) {
return false;
}
$album = $album[0];

// The album owner may add to their own album.
if ( $user && intval( $album->media_author ) === $user ) {
return true;
}

// Group albums: users who may post in the owning group may add.
if ( 'group' === $album->context ) {
return $this->can_user_upload_in_target_group( $user, intval( $album->context_id ) );
}

return false;
}

/**
* Validate the upload's context and album against the acting user, falling back to
* safe defaults (the user's own profile / default album) when not permitted.
*/
public function authorize_targets() {
$user = intval( $this->upload['media_author'] );

if ( 'profile' === $this->upload['context'] || 'bp_member' === $this->upload['context'] ) {
// A user may only upload to their own profile.
$this->upload['context'] = 'profile';
$this->upload['context_id'] = $user;
} elseif ( 'group' === $this->upload['context'] ) {
// Only users who may post in the target group may upload into it.
if ( ! $this->can_user_upload_in_target_group( $user, intval( $this->upload['context_id'] ) ) ) {
$this->upload['context'] = 'profile';
$this->upload['context_id'] = $user;
}
}

if ( ! $this->has_album_id() || ! $this->has_album_permissions() ) {
$this->set_album_id();
}
}

/**
* Whether a user may upload into a given BuddyPress group (members, or site admins).
*
* @param int $user User id.
* @param int $group_id Group id.
*
* @return boolean
*/
private function can_user_upload_in_target_group( $user, $group_id ) {
if ( $user && user_can( $user, 'list_users' ) ) {
return true;
}

$allowed = ( $user && $group_id && function_exists( 'groups_is_user_member' ) )
? (bool) groups_is_user_member( $user, $group_id )
: false;

return (bool) apply_filters( 'rtm_can_user_upload_in_group', $allowed, $group_id, $user );
}

/**
Expand Down
2 changes: 1 addition & 1 deletion app/main/routers/query/RTMediaQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ public function &query( $query ) {
if ( ! empty( $rtmedia_shortcode ) ) {
$query_data = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.NoNonceVerification -- We are just replacing the args so nonce verification is not needed.
foreach ( $query_data as $key => $val ) {
if ( ! in_array( $key, $allowed_query, true ) ) {
if ( ! in_array( $key, $allowed_query, true ) || is_array( $val ) ) {
unset( $query_data[ $key ] );
}
}
Expand Down
Loading