diff --git a/content.py b/content.py index 0782d0ed0..44113ac59 100644 --- a/content.py +++ b/content.py @@ -151,7 +151,7 @@ def htmlReplaceQuoteMarks(content: str) -> str: return newContent -def dangerousMarkup(content: str) -> bool: +def dangerousMarkup(content: str, allowLocalNetworkAccess: bool) -> bool: """Returns true if the given content contains dangerous html markup """ if '<' not in content: @@ -159,7 +159,9 @@ def dangerousMarkup(content: str) -> bool: if '>' not in content: return False contentSections = content.split('<') - invalidPartials = ('127.0.', '192.168', '10.0.') + invalidPartials = () + if not allowLocalNetworkAccess: + invalidPartials = ('127.0.', '192.168', '10.0.') invalidStrings = ('script', 'canvas', 'style', 'abbr', 'frame', 'iframe', 'html', 'body', 'hr') @@ -181,7 +183,7 @@ def dangerousMarkup(content: str) -> bool: return False -def dangerousCSS(filename: str) -> bool: +def dangerousCSS(filename: str, allowLocalNetworkAccess: bool) -> bool: """Returns true is the css file contains code which can create security problems """ @@ -199,7 +201,7 @@ def dangerousCSS(filename: str) -> bool: # an attacker can include html inside of the css # file as a comment and this may then be run from the html - if dangerousMarkup(content): + if dangerousMarkup(content, allowLocalNetworkAccess): return True return False diff --git a/daemon.py b/daemon.py index 4fd08b73c..4c1b79141 100644 --- a/daemon.py +++ b/daemon.py @@ -2917,7 +2917,10 @@ class PubServer(BaseHTTPRequestHandler): if nickname == adminNickname: if fields.get('editedAbout'): aboutStr = fields['editedAbout'] - if not dangerousMarkup(aboutStr): + allowLocalNetworkAccess = \ + self.server.allowLocalNetworkAccess + if not dangerousMarkup(aboutStr, + allowLocalNetworkAccess): aboutFile = open(aboutFilename, "w+") if aboutFile: aboutFile.write(aboutStr) @@ -2928,7 +2931,10 @@ class PubServer(BaseHTTPRequestHandler): if fields.get('editedTOS'): TOSStr = fields['editedTOS'] - if not dangerousMarkup(TOSStr): + allowLocalNetworkAccess = \ + self.server.allowLocalNetworkAccess + if not dangerousMarkup(TOSStr, + allowLocalNetworkAccess): TOSFile = open(TOSFilename, "w+") if TOSFile: TOSFile.write(TOSStr) @@ -3655,7 +3661,8 @@ class PubServer(BaseHTTPRequestHandler): if fields.get('themeDropdown'): setTheme(baseDir, fields['themeDropdown'], - domain) + domain. + self.server.allowLocalNetworkAccess) self.server.showPublishAsIcon = \ getConfigParam(self.server.baseDir, 'showPublishAsIcon') @@ -4014,7 +4021,8 @@ class PubServer(BaseHTTPRequestHandler): '.etag') currTheme = getTheme(baseDir) if currTheme: - setTheme(baseDir, currTheme, domain) + setTheme(baseDir, currTheme, domain, + self.server.allowLocalNetworkAccess) self.server.showPublishAsIcon = \ getConfigParam(self.server.baseDir, 'showPublishAsIcon') @@ -12374,7 +12382,8 @@ def loadTokens(baseDir: str, tokensDict: {}, tokensLookup: {}) -> None: tokensLookup[token] = nickname -def runDaemon(maxFeedItemSizeKb: int, +def runDaemon(allowLocalNetworkAccess: bool, + maxFeedItemSizeKb: int, publishButtonAtTop: bool, rssIconAtTop: bool, iconsAsButtons: bool, @@ -12439,6 +12448,10 @@ def runDaemon(maxFeedItemSizeKb: int, return False httpd.unitTest = unitTest + httpd.allowLocalNetworkAccess = allowLocalNetworkAccess + if unitTest: + # unit tests are run on the local network with LAN addresses + httpd.allowLocalNetworkAccess = True httpd.YTReplacementDomain = YTReplacementDomain # newswire storing rss feeds @@ -12702,7 +12715,8 @@ def runDaemon(maxFeedItemSizeKb: int, httpd.YTReplacementDomain, httpd.showPublishedDateOnly, httpd.allowNewsFollowers, - httpd.maxFollowers), daemon=True) + httpd.maxFollowers, + httpd.allowLocalNetworkAccess), daemon=True) print('Creating scheduled post thread') httpd.thrPostSchedule = \ diff --git a/epicyon.py b/epicyon.py index 9bc1144e4..edc7fe204 100644 --- a/epicyon.py +++ b/epicyon.py @@ -249,6 +249,13 @@ parser.add_argument("--publishButtonAtTop", const=True, default=False, help="Whether to show the publish button at the top of " + "the newswire column") +parser.add_argument("--allowLocalNetworkAccess", + dest='allowLocalNetworkAccess', + type=str2bool, nargs='?', + const=True, default=False, + help="Whether to allow access to local network " + + "addresses. This might be useful when deploying in " + + "a mesh network") parser.add_argument("--noapproval", type=str2bool, nargs='?', const=True, default=False, help="Allow followers without approval") @@ -2059,11 +2066,12 @@ if YTDomain: if '.' in YTDomain: args.YTReplacementDomain = YTDomain -if setTheme(baseDir, themeName, domain): +if setTheme(baseDir, themeName, domain, args.allowLocalNetworkAccess): print('Theme set to ' + themeName) if __name__ == "__main__": - runDaemon(args.maxFeedItemSizeKb, + runDaemon(args.allowLocalNetworkAccess, + args.maxFeedItemSizeKb, args.publishButtonAtTop, args.rssIconAtTop, args.iconsAsButtons, diff --git a/inbox.py b/inbox.py index 42616d809..258e16e19 100644 --- a/inbox.py +++ b/inbox.py @@ -1565,7 +1565,8 @@ def estimateNumberOfEmoji(content: str) -> int: def validPostContent(baseDir: str, nickname: str, domain: str, - messageJson: {}, maxMentions: int, maxEmoji: int) -> bool: + messageJson: {}, maxMentions: int, maxEmoji: int, + allowLocalNetworkAccess: bool) -> bool: """Is the content of a received post valid? Check for bad html Check for hellthreads @@ -1600,7 +1601,8 @@ def validPostContent(baseDir: str, nickname: str, domain: str, messageJson['object']['content']): return True - if dangerousMarkup(messageJson['object']['content']): + if dangerousMarkup(messageJson['object']['content'], + allowLocalNetworkAccess): if messageJson['object'].get('id'): print('REJECT ARBITRARY HTML: ' + messageJson['object']['id']) print('REJECT ARBITRARY HTML: bad string in post - ' + @@ -2030,7 +2032,8 @@ def inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, maxReplies: int, allowDeletion: bool, maxMentions: int, maxEmoji: int, translate: {}, unitTest: bool, YTReplacementDomain: str, - showPublishedDateOnly: bool) -> bool: + showPublishedDateOnly: bool, + allowLocalNetworkAccess: bool) -> bool: """ Anything which needs to be done after initial checks have passed """ actor = keyId @@ -2155,7 +2158,8 @@ def inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, nickname = handle.split('@')[0] if validPostContent(baseDir, nickname, domain, - postJsonObject, maxMentions, maxEmoji): + postJsonObject, maxMentions, maxEmoji, + allowLocalNetworkAccess): if postJsonObject.get('object'): jsonObj = postJsonObject['object'] @@ -2438,7 +2442,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, YTReplacementDomain: str, showPublishedDateOnly: bool, allowNewsFollowers: bool, - maxFollowers: int) -> None: + maxFollowers: int, allowLocalNetworkAccess: bool) -> None: """Processes received items and moves them to the appropriate directories """ @@ -2853,7 +2857,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, maxMentions, maxEmoji, translate, unitTest, YTReplacementDomain, - showPublishedDateOnly) + showPublishedDateOnly, + allowLocalNetworkAccess) if debug: pprint(queueJson['post']) diff --git a/newsdaemon.py b/newsdaemon.py index 7b9b43430..899866179 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -469,7 +469,8 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, personCache: {}, federationList: [], sendThreads: [], postLog: [], - maxMirroredArticles: int) -> None: + maxMirroredArticles: int, + allowLocalNetworkAccess: bool) -> None: """Converts rss items in a newswire into posts """ if not newswire: @@ -512,7 +513,8 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, rssTitle = removeControlCharacters(item[0]) url = item[1] - if dangerousMarkup(url) or dangerousMarkup(rssTitle): + if dangerousMarkup(url, allowLocalNetworkAccess) or \ + dangerousMarkup(rssTitle, allowLocalNetworkAccess): continue rssDescription = '' @@ -537,7 +539,8 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, postUrl += '/index.html' # add the off-site link to the description - if rssDescription and not dangerousMarkup(rssDescription): + if rssDescription and \ + not dangerousMarkup(rssDescription, allowLocalNetworkAccess): rssDescription += \ '
' + \ translate['Read more...'] + '' @@ -743,7 +746,8 @@ def runNewswireDaemon(baseDir: str, httpd, httpd.federationList, httpd.sendThreads, httpd.postLog, - httpd.maxMirroredArticles) + httpd.maxMirroredArticles, + httpd.allowLocalNetworkAccess) print('Newswire feed converted to ActivityPub') if httpd.maxNewsPosts > 0: diff --git a/tests.py b/tests.py index 893fc036d..c2de44e8c 100644 --- a/tests.py +++ b/tests.py @@ -291,8 +291,10 @@ def createServerAlice(path: str, domain: str, port: int, maxEmoji = 10 onionDomain = None i2pDomain = None + allowLocalNetworkAccess = True print('Server running: Alice') - runDaemon(2048, False, True, False, False, True, 10, False, + runDaemon(allowLocalNetworkAccess, + 2048, False, True, False, False, True, 10, False, 0, 100, 1024, 5, False, 0, False, 1, False, False, False, 5, True, True, 'en', __version__, @@ -356,8 +358,10 @@ def createServerBob(path: str, domain: str, port: int, maxEmoji = 10 onionDomain = None i2pDomain = None + allowLocalNetworkAccess = True print('Server running: Bob') - runDaemon(2048, False, True, False, False, True, 10, False, + runDaemon(allowLocalNetworkAccess, + 2048, False, True, False, False, True, 10, False, 0, 100, 1024, 5, False, 0, False, 1, False, False, False, 5, True, True, 'en', __version__, @@ -395,8 +399,10 @@ def createServerEve(path: str, domain: str, port: int, federationList: [], maxEmoji = 10 onionDomain = None i2pDomain = None + allowLocalNetworkAccess = True print('Server running: Eve') - runDaemon(2048, False, True, False, False, True, 10, False, + runDaemon(allowLocalNetworkAccess, + 2048, False, True, False, False, True, 10, False, 0, 100, 1024, 5, False, 0, False, 1, False, False, False, 5, True, True, 'en', __version__, @@ -1941,58 +1947,59 @@ def testRemoveHtml(): def testDangerousMarkup(): print('testDangerousMarkup') + allowLocalNetworkAccess = False content = '

This is a valid message

' - assert(not dangerousMarkup(content)) + assert(not dangerousMarkup(content, allowLocalNetworkAccess)) content = 'This is a valid message without markup' - assert(not dangerousMarkup(content)) + assert(not dangerousMarkup(content, allowLocalNetworkAccess)) content = '

This is a valid-looking message. But wait... ' + \ '

' - assert(dangerousMarkup(content)) + assert(dangerousMarkup(content, allowLocalNetworkAccess)) content = '

This html contains more than you expected... ' + \ '

' - assert(dangerousMarkup(content)) + assert(dangerousMarkup(content, allowLocalNetworkAccess)) content = '

This is a valid-looking message. But wait... ' + \ '