Option to allow access to the local network

This might be useful for mesh networks or private networks
main
Bob Mottram 2020-11-20 10:58:49 +00:00
parent b889c8dfdd
commit 5364b71616
7 changed files with 97 additions and 49 deletions

View File

@ -151,7 +151,7 @@ def htmlReplaceQuoteMarks(content: str) -> str:
return newContent return newContent
def dangerousMarkup(content: str) -> bool: def dangerousMarkup(content: str, allowLocalNetworkAccess: bool) -> bool:
"""Returns true if the given content contains dangerous html markup """Returns true if the given content contains dangerous html markup
""" """
if '<' not in content: if '<' not in content:
@ -159,7 +159,9 @@ def dangerousMarkup(content: str) -> bool:
if '>' not in content: if '>' not in content:
return False return False
contentSections = content.split('<') 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', invalidStrings = ('script', 'canvas', 'style', 'abbr',
'frame', 'iframe', 'html', 'body', 'frame', 'iframe', 'html', 'body',
'hr') 'hr')
@ -181,7 +183,7 @@ def dangerousMarkup(content: str) -> bool:
return False return False
def dangerousCSS(filename: str) -> bool: def dangerousCSS(filename: str, allowLocalNetworkAccess: bool) -> bool:
"""Returns true is the css file contains code which """Returns true is the css file contains code which
can create security problems can create security problems
""" """
@ -199,7 +201,7 @@ def dangerousCSS(filename: str) -> bool:
# an attacker can include html inside of the css # an attacker can include html inside of the css
# file as a comment and this may then be run from the html # file as a comment and this may then be run from the html
if dangerousMarkup(content): if dangerousMarkup(content, allowLocalNetworkAccess):
return True return True
return False return False

View File

@ -2917,7 +2917,10 @@ class PubServer(BaseHTTPRequestHandler):
if nickname == adminNickname: if nickname == adminNickname:
if fields.get('editedAbout'): if fields.get('editedAbout'):
aboutStr = fields['editedAbout'] aboutStr = fields['editedAbout']
if not dangerousMarkup(aboutStr): allowLocalNetworkAccess = \
self.server.allowLocalNetworkAccess
if not dangerousMarkup(aboutStr,
allowLocalNetworkAccess):
aboutFile = open(aboutFilename, "w+") aboutFile = open(aboutFilename, "w+")
if aboutFile: if aboutFile:
aboutFile.write(aboutStr) aboutFile.write(aboutStr)
@ -2928,7 +2931,10 @@ class PubServer(BaseHTTPRequestHandler):
if fields.get('editedTOS'): if fields.get('editedTOS'):
TOSStr = fields['editedTOS'] TOSStr = fields['editedTOS']
if not dangerousMarkup(TOSStr): allowLocalNetworkAccess = \
self.server.allowLocalNetworkAccess
if not dangerousMarkup(TOSStr,
allowLocalNetworkAccess):
TOSFile = open(TOSFilename, "w+") TOSFile = open(TOSFilename, "w+")
if TOSFile: if TOSFile:
TOSFile.write(TOSStr) TOSFile.write(TOSStr)
@ -3655,7 +3661,8 @@ class PubServer(BaseHTTPRequestHandler):
if fields.get('themeDropdown'): if fields.get('themeDropdown'):
setTheme(baseDir, setTheme(baseDir,
fields['themeDropdown'], fields['themeDropdown'],
domain) domain.
self.server.allowLocalNetworkAccess)
self.server.showPublishAsIcon = \ self.server.showPublishAsIcon = \
getConfigParam(self.server.baseDir, getConfigParam(self.server.baseDir,
'showPublishAsIcon') 'showPublishAsIcon')
@ -4014,7 +4021,8 @@ class PubServer(BaseHTTPRequestHandler):
'.etag') '.etag')
currTheme = getTheme(baseDir) currTheme = getTheme(baseDir)
if currTheme: if currTheme:
setTheme(baseDir, currTheme, domain) setTheme(baseDir, currTheme, domain,
self.server.allowLocalNetworkAccess)
self.server.showPublishAsIcon = \ self.server.showPublishAsIcon = \
getConfigParam(self.server.baseDir, getConfigParam(self.server.baseDir,
'showPublishAsIcon') 'showPublishAsIcon')
@ -12374,7 +12382,8 @@ def loadTokens(baseDir: str, tokensDict: {}, tokensLookup: {}) -> None:
tokensLookup[token] = nickname tokensLookup[token] = nickname
def runDaemon(maxFeedItemSizeKb: int, def runDaemon(allowLocalNetworkAccess: bool,
maxFeedItemSizeKb: int,
publishButtonAtTop: bool, publishButtonAtTop: bool,
rssIconAtTop: bool, rssIconAtTop: bool,
iconsAsButtons: bool, iconsAsButtons: bool,
@ -12439,6 +12448,10 @@ def runDaemon(maxFeedItemSizeKb: int,
return False return False
httpd.unitTest = unitTest 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 httpd.YTReplacementDomain = YTReplacementDomain
# newswire storing rss feeds # newswire storing rss feeds
@ -12702,7 +12715,8 @@ def runDaemon(maxFeedItemSizeKb: int,
httpd.YTReplacementDomain, httpd.YTReplacementDomain,
httpd.showPublishedDateOnly, httpd.showPublishedDateOnly,
httpd.allowNewsFollowers, httpd.allowNewsFollowers,
httpd.maxFollowers), daemon=True) httpd.maxFollowers,
httpd.allowLocalNetworkAccess), daemon=True)
print('Creating scheduled post thread') print('Creating scheduled post thread')
httpd.thrPostSchedule = \ httpd.thrPostSchedule = \

View File

@ -249,6 +249,13 @@ parser.add_argument("--publishButtonAtTop",
const=True, default=False, const=True, default=False,
help="Whether to show the publish button at the top of " + help="Whether to show the publish button at the top of " +
"the newswire column") "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='?', parser.add_argument("--noapproval", type=str2bool, nargs='?',
const=True, default=False, const=True, default=False,
help="Allow followers without approval") help="Allow followers without approval")
@ -2059,11 +2066,12 @@ if YTDomain:
if '.' in YTDomain: if '.' in YTDomain:
args.YTReplacementDomain = YTDomain args.YTReplacementDomain = YTDomain
if setTheme(baseDir, themeName, domain): if setTheme(baseDir, themeName, domain, args.allowLocalNetworkAccess):
print('Theme set to ' + themeName) print('Theme set to ' + themeName)
if __name__ == "__main__": if __name__ == "__main__":
runDaemon(args.maxFeedItemSizeKb, runDaemon(args.allowLocalNetworkAccess,
args.maxFeedItemSizeKb,
args.publishButtonAtTop, args.publishButtonAtTop,
args.rssIconAtTop, args.rssIconAtTop,
args.iconsAsButtons, args.iconsAsButtons,

View File

@ -1565,7 +1565,8 @@ def estimateNumberOfEmoji(content: str) -> int:
def validPostContent(baseDir: str, nickname: str, domain: str, 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? """Is the content of a received post valid?
Check for bad html Check for bad html
Check for hellthreads Check for hellthreads
@ -1600,7 +1601,8 @@ def validPostContent(baseDir: str, nickname: str, domain: str,
messageJson['object']['content']): messageJson['object']['content']):
return True return True
if dangerousMarkup(messageJson['object']['content']): if dangerousMarkup(messageJson['object']['content'],
allowLocalNetworkAccess):
if messageJson['object'].get('id'): if messageJson['object'].get('id'):
print('REJECT ARBITRARY HTML: ' + messageJson['object']['id']) print('REJECT ARBITRARY HTML: ' + messageJson['object']['id'])
print('REJECT ARBITRARY HTML: bad string in post - ' + print('REJECT ARBITRARY HTML: bad string in post - ' +
@ -2030,7 +2032,8 @@ def inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
maxReplies: int, allowDeletion: bool, maxReplies: int, allowDeletion: bool,
maxMentions: int, maxEmoji: int, translate: {}, maxMentions: int, maxEmoji: int, translate: {},
unitTest: bool, YTReplacementDomain: str, unitTest: bool, YTReplacementDomain: str,
showPublishedDateOnly: bool) -> bool: showPublishedDateOnly: bool,
allowLocalNetworkAccess: bool) -> bool:
""" Anything which needs to be done after initial checks have passed """ Anything which needs to be done after initial checks have passed
""" """
actor = keyId actor = keyId
@ -2155,7 +2158,8 @@ def inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
nickname = handle.split('@')[0] nickname = handle.split('@')[0]
if validPostContent(baseDir, nickname, domain, if validPostContent(baseDir, nickname, domain,
postJsonObject, maxMentions, maxEmoji): postJsonObject, maxMentions, maxEmoji,
allowLocalNetworkAccess):
if postJsonObject.get('object'): if postJsonObject.get('object'):
jsonObj = postJsonObject['object'] jsonObj = postJsonObject['object']
@ -2438,7 +2442,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
YTReplacementDomain: str, YTReplacementDomain: str,
showPublishedDateOnly: bool, showPublishedDateOnly: bool,
allowNewsFollowers: bool, allowNewsFollowers: bool,
maxFollowers: int) -> None: maxFollowers: int, allowLocalNetworkAccess: bool) -> None:
"""Processes received items and moves them to the appropriate """Processes received items and moves them to the appropriate
directories directories
""" """
@ -2853,7 +2857,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
maxMentions, maxEmoji, maxMentions, maxEmoji,
translate, unitTest, translate, unitTest,
YTReplacementDomain, YTReplacementDomain,
showPublishedDateOnly) showPublishedDateOnly,
allowLocalNetworkAccess)
if debug: if debug:
pprint(queueJson['post']) pprint(queueJson['post'])

View File

@ -469,7 +469,8 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str,
personCache: {}, personCache: {},
federationList: [], federationList: [],
sendThreads: [], postLog: [], sendThreads: [], postLog: [],
maxMirroredArticles: int) -> None: maxMirroredArticles: int,
allowLocalNetworkAccess: bool) -> None:
"""Converts rss items in a newswire into posts """Converts rss items in a newswire into posts
""" """
if not newswire: if not newswire:
@ -512,7 +513,8 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str,
rssTitle = removeControlCharacters(item[0]) rssTitle = removeControlCharacters(item[0])
url = item[1] url = item[1]
if dangerousMarkup(url) or dangerousMarkup(rssTitle): if dangerousMarkup(url, allowLocalNetworkAccess) or \
dangerousMarkup(rssTitle, allowLocalNetworkAccess):
continue continue
rssDescription = '' rssDescription = ''
@ -537,7 +539,8 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str,
postUrl += '/index.html' postUrl += '/index.html'
# add the off-site link to the description # add the off-site link to the description
if rssDescription and not dangerousMarkup(rssDescription): if rssDescription and \
not dangerousMarkup(rssDescription, allowLocalNetworkAccess):
rssDescription += \ rssDescription += \
'<br><a href="' + postUrl + '">' + \ '<br><a href="' + postUrl + '">' + \
translate['Read more...'] + '</a>' translate['Read more...'] + '</a>'
@ -743,7 +746,8 @@ def runNewswireDaemon(baseDir: str, httpd,
httpd.federationList, httpd.federationList,
httpd.sendThreads, httpd.sendThreads,
httpd.postLog, httpd.postLog,
httpd.maxMirroredArticles) httpd.maxMirroredArticles,
httpd.allowLocalNetworkAccess)
print('Newswire feed converted to ActivityPub') print('Newswire feed converted to ActivityPub')
if httpd.maxNewsPosts > 0: if httpd.maxNewsPosts > 0:

View File

@ -291,8 +291,10 @@ def createServerAlice(path: str, domain: str, port: int,
maxEmoji = 10 maxEmoji = 10
onionDomain = None onionDomain = None
i2pDomain = None i2pDomain = None
allowLocalNetworkAccess = True
print('Server running: Alice') 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, 100, 1024, 5, False,
0, False, 1, False, False, False, 0, False, 1, False, False, False,
5, True, True, 'en', __version__, 5, True, True, 'en', __version__,
@ -356,8 +358,10 @@ def createServerBob(path: str, domain: str, port: int,
maxEmoji = 10 maxEmoji = 10
onionDomain = None onionDomain = None
i2pDomain = None i2pDomain = None
allowLocalNetworkAccess = True
print('Server running: Bob') 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, 0, 100, 1024, 5, False, 0,
False, 1, False, False, False, False, 1, False, False, False,
5, True, True, 'en', __version__, 5, True, True, 'en', __version__,
@ -395,8 +399,10 @@ def createServerEve(path: str, domain: str, port: int, federationList: [],
maxEmoji = 10 maxEmoji = 10
onionDomain = None onionDomain = None
i2pDomain = None i2pDomain = None
allowLocalNetworkAccess = True
print('Server running: Eve') 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, 0, 100, 1024, 5, False, 0,
False, 1, False, False, False, False, 1, False, False, False,
5, True, True, 'en', __version__, 5, True, True, 'en', __version__,
@ -1941,58 +1947,59 @@ def testRemoveHtml():
def testDangerousMarkup(): def testDangerousMarkup():
print('testDangerousMarkup') print('testDangerousMarkup')
allowLocalNetworkAccess = False
content = '<p>This is a valid message</p>' content = '<p>This is a valid message</p>'
assert(not dangerousMarkup(content)) assert(not dangerousMarkup(content, allowLocalNetworkAccess))
content = 'This is a valid message without markup' content = 'This is a valid message without markup'
assert(not dangerousMarkup(content)) assert(not dangerousMarkup(content, allowLocalNetworkAccess))
content = '<p>This is a valid-looking message. But wait... ' + \ content = '<p>This is a valid-looking message. But wait... ' + \
'<script>document.getElementById("concentrated")' + \ '<script>document.getElementById("concentrated")' + \
'.innerHTML = "evil";</script></p>' '.innerHTML = "evil";</script></p>'
assert(dangerousMarkup(content)) assert(dangerousMarkup(content, allowLocalNetworkAccess))
content = '<p>This html contains more than you expected... ' + \ content = '<p>This html contains more than you expected... ' + \
'<script language="javascript">document.getElementById("abc")' + \ '<script language="javascript">document.getElementById("abc")' + \
'.innerHTML = "def";</script></p>' '.innerHTML = "def";</script></p>'
assert(dangerousMarkup(content)) assert(dangerousMarkup(content, allowLocalNetworkAccess))
content = '<p>This is a valid-looking message. But wait... ' + \ content = '<p>This is a valid-looking message. But wait... ' + \
'<script src="https://evilsite/payload.js" /></p>' '<script src="https://evilsite/payload.js" /></p>'
assert(dangerousMarkup(content)) assert(dangerousMarkup(content, allowLocalNetworkAccess))
content = '<p>This message embeds an evil frame.' + \ content = '<p>This message embeds an evil frame.' + \
'<iframe src="somesite"></iframe></p>' '<iframe src="somesite"></iframe></p>'
assert(dangerousMarkup(content)) assert(dangerousMarkup(content, allowLocalNetworkAccess))
content = '<p>This message tries to obfuscate an evil frame.' + \ content = '<p>This message tries to obfuscate an evil frame.' + \
'< iframe src = "somesite"></ iframe ></p>' '< iframe src = "somesite"></ iframe ></p>'
assert(dangerousMarkup(content)) assert(dangerousMarkup(content, allowLocalNetworkAccess))
content = '<p>This message is not necessarily evil, but annoying.' + \ content = '<p>This message is not necessarily evil, but annoying.' + \
'<hr><br><br><br><br><br><br><br><hr><hr></p>' '<hr><br><br><br><br><br><br><br><hr><hr></p>'
assert(dangerousMarkup(content)) assert(dangerousMarkup(content, allowLocalNetworkAccess))
content = '<p>This message contans a ' + \ content = '<p>This message contans a ' + \
'<a href="https://validsite/index.html">valid link.</a></p>' '<a href="https://validsite/index.html">valid link.</a></p>'
assert(not dangerousMarkup(content)) assert(not dangerousMarkup(content, allowLocalNetworkAccess))
content = '<p>This message contans a ' + \ content = '<p>This message contans a ' + \
'<a href="https://validsite/iframe.html">' + \ '<a href="https://validsite/iframe.html">' + \
'valid link having invalid but harmless name.</a></p>' 'valid link having invalid but harmless name.</a></p>'
assert(not dangerousMarkup(content)) assert(not dangerousMarkup(content, allowLocalNetworkAccess))
content = '<p>This message which <a href="127.0.0.1:8736">' + \ content = '<p>This message which <a href="127.0.0.1:8736">' + \
'tries to access the local network</a></p>' 'tries to access the local network</a></p>'
assert(dangerousMarkup(content)) assert(dangerousMarkup(content, allowLocalNetworkAccess))
content = '<p>This message which <a href="http://192.168.5.10:7235">' + \ content = '<p>This message which <a href="http://192.168.5.10:7235">' + \
'tries to access the local network</a></p>' 'tries to access the local network</a></p>'
assert(dangerousMarkup(content)) assert(dangerousMarkup(content, allowLocalNetworkAccess))
content = '<p>127.0.0.1 This message which does not access ' + \ content = '<p>127.0.0.1 This message which does not access ' + \
'the local network</a></p>' 'the local network</a></p>'
assert(not dangerousMarkup(content)) assert(not dangerousMarkup(content, allowLocalNetworkAccess))
def runHtmlReplaceQuoteMarks(): def runHtmlReplaceQuoteMarks():

View File

@ -183,7 +183,8 @@ def setCSSparam(css: str, param: str, value: str) -> str:
def setThemeFromDict(baseDir: str, name: str, def setThemeFromDict(baseDir: str, name: str,
themeParams: {}, bgParams: {}) -> None: themeParams: {}, bgParams: {},
allowLocalNetworkAccess: bool) -> None:
"""Uses a dictionary to set a theme """Uses a dictionary to set a theme
""" """
if name: if name:
@ -198,7 +199,7 @@ def setThemeFromDict(baseDir: str, name: str,
# Ensure that any custom CSS is mostly harmless. # Ensure that any custom CSS is mostly harmless.
# If not then just use the defaults # If not then just use the defaults
if dangerousCSS(templateFilename) or \ if dangerousCSS(templateFilename, allowLocalNetworkAccess) or \
not os.path.isfile(templateFilename): not os.path.isfile(templateFilename):
# use default css # use default css
templateFilename = baseDir + '/epicyon-' + filename templateFilename = baseDir + '/epicyon-' + filename
@ -355,7 +356,8 @@ def setCustomFont(baseDir: str):
def readVariablesFile(baseDir: str, themeName: str, def readVariablesFile(baseDir: str, themeName: str,
variablesFile: str) -> None: variablesFile: str,
allowLocalNetworkAccess: bool) -> None:
"""Reads variables from a file in the theme directory """Reads variables from a file in the theme directory
""" """
themeParams = loadJson(variablesFile, 0) themeParams = loadJson(variablesFile, 0)
@ -367,10 +369,11 @@ def readVariablesFile(baseDir: str, themeName: str,
"options": "jpg", "options": "jpg",
"search": "jpg" "search": "jpg"
} }
setThemeFromDict(baseDir, themeName, themeParams, bgParams) setThemeFromDict(baseDir, themeName, themeParams, bgParams,
allowLocalNetworkAccess)
def setThemeDefault(baseDir: str): def setThemeDefault(baseDir: str, allowLocalNetworkAccess: bool):
name = 'default' name = 'default'
removeTheme(baseDir) removeTheme(baseDir)
setThemeInConfig(baseDir, name) setThemeInConfig(baseDir, name)
@ -390,10 +393,11 @@ def setThemeDefault(baseDir: str):
"banner-height-mobile": "10vh", "banner-height-mobile": "10vh",
"search-banner-height-mobile": "15vh" "search-banner-height-mobile": "15vh"
} }
setThemeFromDict(baseDir, name, themeParams, bgParams) setThemeFromDict(baseDir, name, themeParams, bgParams,
allowLocalNetworkAccess)
def setThemeHighVis(baseDir: str): def setThemeHighVis(baseDir: str, allowLocalNetworkAccess: bool):
name = 'highvis' name = 'highvis'
themeParams = { themeParams = {
"newswire-publish-icon": True, "newswire-publish-icon": True,
@ -422,7 +426,8 @@ def setThemeHighVis(baseDir: str):
"options": "jpg", "options": "jpg",
"search": "jpg" "search": "jpg"
} }
setThemeFromDict(baseDir, name, themeParams, bgParams) setThemeFromDict(baseDir, name, themeParams, bgParams,
allowLocalNetworkAccess)
def setThemeFonts(baseDir: str, themeName: str) -> None: def setThemeFonts(baseDir: str, themeName: str) -> None:
@ -578,7 +583,8 @@ def setNewsAvatar(baseDir: str, name: str,
nickname + '@' + domain + '/avatar.png') nickname + '@' + domain + '/avatar.png')
def setTheme(baseDir: str, name: str, domain: str) -> bool: def setTheme(baseDir: str, name: str, domain: str,
allowLocalNetworkAccess: bool) -> bool:
result = False result = False
prevThemeName = getTheme(baseDir) prevThemeName = getTheme(baseDir)
@ -589,7 +595,8 @@ def setTheme(baseDir: str, name: str, domain: str) -> bool:
themeNameLower = themeName.lower() themeNameLower = themeName.lower()
if name == themeNameLower: if name == themeNameLower:
try: try:
globals()['setTheme' + themeName](baseDir) globals()['setTheme' + themeName](baseDir,
allowLocalNetworkAccess)
except BaseException: except BaseException:
pass pass
@ -608,7 +615,8 @@ def setTheme(baseDir: str, name: str, domain: str) -> bool:
variablesFile = baseDir + '/theme/' + name + '/theme.json' variablesFile = baseDir + '/theme/' + name + '/theme.json'
if os.path.isfile(variablesFile): if os.path.isfile(variablesFile):
readVariablesFile(baseDir, name, variablesFile) readVariablesFile(baseDir, name, variablesFile,
allowLocalNetworkAccess)
setCustomFont(baseDir) setCustomFont(baseDir)