Fetch and verify an object from its source if sent without a signature

This commit is contained in:
freamon 2025-01-06 19:13:23 +00:00
parent 26283a5d73
commit af82bc7076
2 changed files with 86 additions and 8 deletions

View file

@ -25,7 +25,7 @@ from app.activitypub.util import public_key, users_total, active_half_year, acti
update_post_from_activity, undo_vote, undo_downvote, post_to_page, get_redis_connection, find_reported_object, \
process_report, ensure_domains_match, can_edit, can_delete, remove_data_from_banned_user, resolve_remote_post, \
inform_followers_of_post_update, comment_model_to_json, restore_post_or_comment, ban_user, unban_user, \
log_incoming_ap, find_community, site_ban_remove_data, community_ban_remove_data
log_incoming_ap, find_community, site_ban_remove_data, community_ban_remove_data, verify_object_from_source
from app.utils import gibberish, get_setting, render_template, \
community_membership, ap_datetime, ip_address, can_downvote, \
can_upvote, can_create_post, awaken_dormant_instance, shorten_string, can_create_post_reply, sha256_digest, \
@ -465,14 +465,19 @@ def shared_inbox():
HttpSignature.verify_request(request, actor.public_key, skip_date=True)
except VerificationError as e:
bounced = True
if not 'signature' in request_json:
log_incoming_ap(id, APLOG_NOTYPE, APLOG_FAILURE, request_json if store_ap_json else None, 'Could not verify HTTP signature: ' + str(e))
return '', 400
# HTTP sig will fail if a.gup.pe or PeerTube have bounced a request, so check LD sig instead
try:
LDSignature.verify_signature(request_json, actor.public_key)
except VerificationError as e:
log_incoming_ap(id, APLOG_NOTYPE, APLOG_FAILURE, request_json if store_ap_json else None, 'Could not verify LD signature: ' + str(e))
if 'signature' in request_json:
try:
LDSignature.verify_signature(request_json, actor.public_key)
except VerificationError as e:
log_incoming_ap(id, APLOG_NOTYPE, APLOG_FAILURE, request_json if store_ap_json else None, 'Could not verify LD signature: ' + str(e))
return '', 400
# not HTTP sig, and no LD sig, so reduce the inner object to just its remote ID, and then fetch it and check it in process_inbox_request()
elif ((request_json['type'] == 'Create' or request_json['type'] == 'Update') and
isinstance(request_json['object'], dict) and 'id' in request_json['object'] and isinstance(request_json['object']['id'], str)):
request_json['object'] = request_json['object']['id']
else:
log_incoming_ap(id, APLOG_NOTYPE, APLOG_FAILURE, request_json if store_ap_json else None, 'Could not verify HTTP signature: ' + str(e))
return '', 400
actor.instance.last_seen = utcnow()
@ -565,6 +570,11 @@ def replay_inbox_request(request_json):
process_delete_request(request_json, True)
return
# testing verify_object_from_source()
if ((request_json['type'] == 'Create' or request_json['type'] == 'Update') and
isinstance(request_json['object'], dict) and 'id' in request_json['object'] and isinstance(request_json['object']['id'], str)):
request_json['object'] = request_json['object']['id']
process_inbox_request(request_json, True)
return
@ -706,6 +716,12 @@ def process_inbox_request(request_json, store_ap_json):
# Create is new content. Update is often an edit, but Updates from Lemmy can also be new content
if request_json['type'] == 'Create' or request_json['type'] == 'Update':
if isinstance(request_json['object'], str):
request_json = verify_object_from_source(request_json) # change request_json['object'] from str to dict, then process normally
if not request_json:
log_incoming_ap(id, APLOG_CREATE, APLOG_FAILURE, request_json if store_ap_json else None, 'Could not verify unsigned request from source')
return
if request_json['object']['type'] == 'ChatMessage':
sender = user
recipient_ap_id = request_json['object']['to'][0]

View file

@ -2467,6 +2467,68 @@ def resolve_remote_post_from_search(uri: str) -> Union[Post, None]:
return None
# called from activitypub/routes if something is posted to us without any kind of signature (typically from PeerTube)
def verify_object_from_source(request_json):
uri = request_json['object']
uri_domain = urlparse(uri).netloc
if not uri_domain:
return None
create_domain = urlparse(request_json['actor']).netloc
if create_domain != uri_domain:
return None
try:
object_request = get_request(uri, headers={'Accept': 'application/activity+json'})
except httpx.HTTPError:
time.sleep(3)
try:
object_request = get_request(uri, headers={'Accept': 'application/activity+json'})
except httpx.HTTPError:
return None
if object_request.status_code == 200:
try:
object = object_request.json()
except:
object_request.close()
return None
object_request.close()
elif object_request.status_code == 401:
try:
site = Site.query.get(1)
object_request = signed_get_request(uri, site.private_key, f"https://{current_app.config['SERVER_NAME']}/actor#main-key")
except httpx.HTTPError:
time.sleep(3)
try:
object_request = signed_get_request(uri, site.private_key, f"https://{current_app.config['SERVER_NAME']}/actor#main-key")
except httpx.HTTPError:
return None
try:
object = object_request.json()
except:
object_request.close()
return None
object_request.close()
else:
return None
if not 'id' in object or not 'type' in object or not 'attributedTo' in object:
return None
if isinstance(object['attributedTo'], str):
actor_domain = urlparse(object['attributedTo']).netloc
elif isinstance(object['attributedTo'], list) and 'id' in object['attributedTo']:
actor_domain = urlparse(object['attributedTo']['id']).netloc
else:
return None
if uri_domain != actor_domain:
return None
request_json['object'] = object
return request_json
# This is for followers on microblog apps
# Used to let them know a Poll has been updated with a new vote
# The plan is to also use it for activities on local user's posts that aren't understood by being Announced (anything beyond the initial Create)