From 865b4c3ce9e32dab57d6aa28bc0dea9124330dfa Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 30 Sep 2019 11:15:20 +0100 Subject: [PATCH] Hellthread mitigation --- daemon.py | 6 ++++-- epicyon.py | 10 +++++++++- inbox.py | 36 +++++++++++++++++++++++++++--------- posts.py | 2 -- tests.py | 9 ++++++--- 5 files changed, 46 insertions(+), 17 deletions(-) diff --git a/daemon.py b/daemon.py index b0c06b44..c74f4837 100644 --- a/daemon.py +++ b/daemon.py @@ -3939,7 +3939,8 @@ def runDaemon(projectVersion, \ instanceId,clientToServer: bool, \ baseDir: str,domain: str, \ port=80,proxyPort=80,httpPrefix='https', \ - fedList=[],authenticatedFetch=False, \ + fedList=[],maxMentions=10, \ + authenticatedFetch=False, \ noreply=False,nolike=False,nopics=False, \ noannounce=False,cw=False,ocapAlways=False, \ useTor=False,maxReplies=64, \ @@ -4078,7 +4079,8 @@ def runDaemon(projectVersion, \ domain,port,useTor,httpd.federationList, \ httpd.ocapAlways,maxReplies, \ domainMaxPostsPerDay,accountMaxPostsPerDay, \ - allowDeletion,debug,httpd.acceptedCaps),daemon=True) + allowDeletion,debug,maxMentions, \ + httpd.acceptedCaps),daemon=True) if not unitTest: httpd.thrWatchdog= \ threadWithTrace(target=runInboxQueueWatchdog, \ diff --git a/epicyon.py b/epicyon.py index d1feb7e5..19ad4ffe 100644 --- a/epicyon.py +++ b/epicyon.py @@ -223,6 +223,8 @@ parser.add_argument("-c","--client", type=str2bool, nargs='?', \ help="Use as an ActivityPub client") parser.add_argument('--maxreplies', dest='maxReplies', type=int,default=64, \ help='Maximum number of replies to a post') +parser.add_argument('--maxMentions','--hellthread', dest='maxMentions', type=int,default=10, \ + help='Maximum number of mentions within a post') parser.add_argument('--role', dest='role', type=str,default=None, \ help='Set a role for a person') parser.add_argument('--organization','--project', dest='project', type=str,default=None, \ @@ -1335,10 +1337,16 @@ if args.testdata: followerOfPerson(baseDir,nickname,domain,'maxboardroom',domainFull,federationList,False) setConfigParam(baseDir,'admin',nickname) +# set a lower bound to the maximum mentions +# so that it can't be accidentally set to zero and disable replies +if args.maxMentions<4: + args.maxMentions=4 + runDaemon(__version__, \ instanceId,args.client,baseDir, \ domain,port,proxyPort,httpPrefix, \ - federationList,args.authenticatedFetch, \ + federationList,args.maxMentions, \ + args.authenticatedFetch, \ args.noreply,args.nolike,args.nopics, \ args.noannounce,args.cw,ocapAlways, \ useTor,args.maxReplies, \ diff --git a/inbox.py b/inbox.py index b84202c9..e8be76d0 100644 --- a/inbox.py +++ b/inbox.py @@ -1096,8 +1096,20 @@ def populateReplies(baseDir :str,httpPrefix :str,domain :str, \ repliesFile.close() return True -def validPostContent(messageJson: {}) -> bool: +def estimateNumberOfMentions(content: str) -> int: + """Returns a rough estimate of the number of mentions + """ + words=content.split(' ') + ctr=0 + for word in words: + if word.startswith('@') or '>@' in word: + ctr+=1 + return ctr + +def validPostContent(messageJson: {},maxMentions: int) -> bool: """Is the content of a received post valid? + Check for bad html + Check for hellthreads """ if not messageJson.get('object'): return True @@ -1112,6 +1124,9 @@ def validPostContent(messageJson: {}) -> bool: print('REJECT: '+messageJson['object']['id']) print('REJECT: bad string in post - '+messageJson['object']['content']) return False + if estimateNumberOfMentions(messageJson['object']['content'])>maxMentions: + print('REJECT: Too many mentions in post - '+messageJson['object']['content']) + return False print('ACCEPT: post content is valid') return True @@ -1120,9 +1135,10 @@ def inboxAfterCapabilities(session,keyId: str,handle: str,messageJson: {}, \ postLog: [],cachedWebfingers: {},personCache: {}, \ queue: [],domain: str,port: int,useTor: bool, \ federationList: [],ocapAlways: bool,debug: bool, \ - acceptedCaps: [], - queueFilename :str,destinationFilename :str, - maxReplies: int,allowDeletion: bool) -> bool: + acceptedCaps: [], \ + queueFilename :str,destinationFilename :str, \ + maxReplies: int,allowDeletion: bool, \ + maxMentions: int) -> bool: """ Anything which needs to be done after capabilities checks have passed """ actor=keyId @@ -1203,11 +1219,11 @@ def inboxAfterCapabilities(session,keyId: str,handle: str,messageJson: {}, \ return True if messageJson.get('postNickname'): - if validPostContent(messageJson['post']): + if validPostContent(messageJson['post'],maxMentions): with open(destinationFilename, 'w+') as fp: commentjson.dump(messageJson['post'], fp, indent=4, sort_keys=False) else: - if validPostContent(messageJson): + if validPostContent(messageJson,maxMentions): with open(destinationFilename, 'w+') as fp: commentjson.dump(messageJson, fp, indent=4, sort_keys=False) @@ -1251,7 +1267,7 @@ def runInboxQueue(projectVersion: str, \ domain: str,port: int,useTor: bool,federationList: [], \ ocapAlways: bool,maxReplies: int, \ domainMaxPostsPerDay: int,accountMaxPostsPerDay: int, \ - allowDeletion: bool,debug: bool, \ + allowDeletion: bool,debug: bool,maxMentions: int, \ acceptedCaps=["inbox:write","objects:read"]) -> None: """Processes received items and moves them to the appropriate directories @@ -1578,7 +1594,8 @@ def runInboxQueue(projectVersion: str, \ federationList,ocapAlways, \ debug,acceptedCaps, \ queueFilename,destination, \ - maxReplies,allowDeletion) + maxReplies,allowDeletion, \ + maxMentions) else: if debug: print('DEBUG: object capabilities check has failed') @@ -1595,7 +1612,8 @@ def runInboxQueue(projectVersion: str, \ federationList,ocapAlways, \ debug,acceptedCaps, \ queueFilename,destination, \ - maxReplies,allowDeletion) + maxReplies,allowDeletion, \ + maxMentions) if debug: pprint(queueJson['post']) print('No capability list within post') diff --git a/posts.py b/posts.py index c9ee33da..15422f7a 100644 --- a/posts.py +++ b/posts.py @@ -461,7 +461,6 @@ def createPostBase(baseDir: str,nickname: str, domain: str, port: int, \ nickname,domain,content, \ mentionedRecipients, \ hashtagsDict) - print('Content tagging stage 1: '+content) statusNumber,published = getStatusNumber() postTo='https://www.w3.org/ns/activitystreams#Public' @@ -504,7 +503,6 @@ def createPostBase(baseDir: str,nickname: str, domain: str, port: int, \ updateHashtagsIndex(baseDir,tag,newPostId) print('Content tags: '+str(tags)) content=replaceEmojiFromTags(content,tags,'content') - print('Content tagging stage 2: '+content) if not clientToServer: actorUrl=httpPrefix+'://'+domain+'/users/'+nickname diff --git a/tests.py b/tests.py index d526b225..6514a71f 100644 --- a/tests.py +++ b/tests.py @@ -216,9 +216,10 @@ def createServerAlice(path: str,domain: str,port: int,federationList: [], \ False, True, clientToServer,None,None,useBlurhash) global testServerAliceRunning testServerAliceRunning = True + maxMentions=10 print('Server running: Alice') runDaemon(__version__,"instanceId",False,path,domain,port,port, \ - httpPrefix,federationList,False, \ + httpPrefix,federationList,maxMentions,False, \ noreply,nolike,nopics,noannounce,cw,ocapAlways, \ useTor,maxReplies, \ domainMaxPostsPerDay,accountMaxPostsPerDay, \ @@ -269,9 +270,10 @@ def createServerBob(path: str,domain: str,port: int,federationList: [], \ False, True, clientToServer,None,None,useBlurhash) global testServerBobRunning testServerBobRunning = True + maxMentions=10 print('Server running: Bob') runDaemon(__version__,"instanceId",False,path,domain,port,port, \ - httpPrefix,federationList,False, \ + httpPrefix,federationList,maxMentions,False, \ noreply,nolike,nopics,noannounce,cw,ocapAlways, \ useTor,maxReplies, \ domainMaxPostsPerDay,accountMaxPostsPerDay, \ @@ -302,9 +304,10 @@ def createServerEve(path: str,domain: str,port: int,federationList: [], \ deleteAllPosts(path,nickname,domain,'outbox') global testServerEveRunning testServerEveRunning = True + maxMentions=10 print('Server running: Eve') runDaemon(__version__,"instanceId",False,path,domain,port,port, \ - httpPrefix,federationList,False, \ + httpPrefix,federationList,maxMentions,False, \ noreply,nolike,nopics,noannounce,cw,ocapAlways, \ useTor,maxReplies,allowDeletion,True,True,False)