chiark / gitweb /
update: strip all metadata from PNGs
[fdroidserver.git] / fdroidserver / update.py
index 71c4e430d4edee755dcae42f10ed0f09a81e4a2c..4611b12761cdee5c5aebe69b11ca069dbbac412f 100644 (file)
@@ -35,7 +35,7 @@ from argparse import ArgumentParser
 import collections
 from binascii import hexlify
 
-from PIL import Image
+from PIL import Image, PngImagePlugin
 import logging
 
 from . import _
@@ -84,6 +84,8 @@ GRAPHIC_NAMES = ('featureGraphic', 'icon', 'promoGraphic', 'tvBanner')
 SCREENSHOT_DIRS = ('phoneScreenshots', 'sevenInchScreenshots',
                    'tenInchScreenshots', 'tvScreenshots', 'wearScreenshots')
 
+BLANK_PNG_INFO = PngImagePlugin.PngInfo()
+
 
 def dpi_to_px(density):
     return (int(density) * 48) / 160
@@ -138,7 +140,7 @@ def update_wiki(apps, sortedids, apks):
             requiresroot = 'Yes'
         else:
             requiresroot = 'No'
-        wikidata += '{{App|id=%s|name=%s|added=%s|lastupdated=%s|source=%s|tracker=%s|web=%s|changelog=%s|donate=%s|flattr=%s|bitcoin=%s|litecoin=%s|license=%s|root=%s|author=%s|email=%s}}\n' % (
+        wikidata += '{{App|id=%s|name=%s|added=%s|lastupdated=%s|source=%s|tracker=%s|web=%s|changelog=%s|donate=%s|flattr=%s|liberapay=%s|bitcoin=%s|litecoin=%s|license=%s|root=%s|author=%s|email=%s}}\n' % (
             appid,
             app.Name,
             app.added.strftime('%Y-%m-%d') if app.added else '',
@@ -149,6 +151,7 @@ def update_wiki(apps, sortedids, apks):
             app.Changelog,
             app.Donate,
             app.FlattrID,
+            app.LiberapayID,
             app.Bitcoin,
             app.Litecoin,
             app.License,
@@ -370,7 +373,8 @@ def resize_icon(iconpath, density):
             im.thumbnail((size, size), Image.ANTIALIAS)
             logging.debug("%s was too large at %s - new size is %s" % (
                 iconpath, oldsize, im.size))
-            im.save(iconpath, "PNG")
+            im.save(iconpath, "PNG", optimize=True,
+                    pnginfo=BLANK_PNG_INFO, icc_profile=None)
 
     except Exception as e:
         logging.error(_("Failed resizing {path}: {error}".format(path=iconpath, error=e)))
@@ -495,14 +499,25 @@ def has_known_vulnerability(filename):
 
     Checks whether there are more than one classes.dex or AndroidManifest.xml
     files, which is invalid and an essential part of the "Master Key" attack.
-
     http://www.saurik.com/id/17
+
+    Janus is similar to Master Key but is perhaps easier to scan for.
+    https://www.guardsquare.com/en/blog/new-android-vulnerability-allows-attackers-modify-apps-without-affecting-their-signatures
     """
 
+    found_vuln = False
+
     # statically load this pattern
     if not hasattr(has_known_vulnerability, "pattern"):
         has_known_vulnerability.pattern = re.compile(b'.*OpenSSL ([01][0-9a-z.-]+)')
 
+    with open(filename.encode(), 'rb') as fp:
+        first4 = fp.read(4)
+    if first4 != b'\x50\x4b\x03\x04':
+        raise FDroidException(_('{path} has bad file signature "{pattern}", possible Janus exploit!')
+                              .format(path=filename, pattern=first4.decode().replace('\n', ' ')) + '\n'
+                              + 'https://www.guardsquare.com/en/blog/new-android-vulnerability-allows-attackers-modify-apps-without-affecting-their-signatures')
+
     files_in_apk = set()
     with zipfile.ZipFile(filename) as zf:
         for name in zf.namelist():
@@ -523,14 +538,15 @@ def has_known_vulnerability(filename):
                         else:
                             logging.warning(_('"{path}" contains outdated {name} ({version})')
                                             .format(path=filename, name=name, version=version))
-                            return True
+                            found_vuln = True
                         break
             elif name == 'AndroidManifest.xml' or name == 'classes.dex' or name.endswith('.so'):
                 if name in files_in_apk:
-                    return True
+                    logging.warning(_('{apkfilename} has multiple {name} files, looks like Master Key exploit!')
+                                    .format(apkfilename=filename, name=name))
+                    found_vuln = True
                 files_in_apk.add(name)
-
-    return False
+    return found_vuln
 
 
 def insert_obbs(repodir, apps, apks):
@@ -659,6 +675,35 @@ def _set_author_entry(app, key, f):
             app[key] = text
 
 
+def _strip_and_copy_image(inpath, outpath):
+    """Remove any metadata from image and copy it to new path
+
+    Sadly, image metadata like EXIF can be used to exploit devices.
+    It is not used at all in the F-Droid ecosystem, so its much safer
+    just to remove it entirely.
+
+    """
+
+    extension = common.get_extension(inpath)[1]
+    if os.path.isdir(outpath):
+        outpath = os.path.join(outpath, os.path.basename(inpath))
+    if extension == 'png':
+        with open(inpath, 'rb') as fp:
+            in_image = Image.open(fp)
+            in_image.save(outpath, "PNG", optimize=True,
+                          pnginfo=BLANK_PNG_INFO, icc_profile=None)
+    elif extension == 'jpg' or extension == 'jpeg':
+        with open(inpath, 'rb') as fp:
+            in_image = Image.open(fp)
+            data = list(in_image.getdata())
+            out_image = Image.new(in_image.mode, in_image.size)
+        out_image.putdata(data)
+        out_image.save(outpath, "JPEG", optimize=True)
+    else:
+        raise FDroidException(_('Unsupported file type "{extension}" for repo graphic')
+                              .format(extension=extension))
+
+
 def copy_triple_t_store_metadata(apps):
     """Include store metadata from the app's source repo
 
@@ -731,7 +776,7 @@ def copy_triple_t_store_metadata(apps):
                         sourcefile = os.path.join(root, f)
                         destfile = os.path.join(destdir, os.path.basename(f))
                         logging.debug('copying ' + sourcefile + ' ' + destfile)
-                        shutil.copy(sourcefile, destfile)
+                        _strip_and_copy_image(sourcefile, destfile)
 
 
 def insert_localized_app_metadata(apps):
@@ -829,7 +874,7 @@ def insert_localized_app_metadata(apps):
                 if base in GRAPHIC_NAMES and extension in ALLOWED_EXTENSIONS:
                     os.makedirs(destdir, mode=0o755, exist_ok=True)
                     logging.debug('copying ' + os.path.join(root, f) + ' ' + destdir)
-                    shutil.copy(os.path.join(root, f), destdir)
+                    _strip_and_copy_image(os.path.join(root, f), destdir)
             for d in dirs:
                 if d in SCREENSHOT_DIRS:
                     if locale == 'images':
@@ -841,7 +886,7 @@ def insert_localized_app_metadata(apps):
                             screenshotdestdir = os.path.join(destdir, d)
                             os.makedirs(screenshotdestdir, mode=0o755, exist_ok=True)
                             logging.debug('copying ' + f + ' ' + screenshotdestdir)
-                            shutil.copy(f, screenshotdestdir)
+                            _strip_and_copy_image(f, screenshotdestdir)
 
     repofiles = sorted(glob.glob(os.path.join('repo', '[A-Za-z]*', '[a-z][a-z][A-Z-.@]*')))
     for d in repofiles:
@@ -1478,7 +1523,8 @@ def fill_missing_icon_densities(empty_densities, icon_filename, apk, repo_dir):
             size = dpi_to_px(density)
 
             im.thumbnail((size, size), Image.ANTIALIAS)
-            im.save(icon_path, "PNG")
+            im.save(icon_path, "PNG", optimize=True,
+                    pnginfo=BLANK_PNG_INFO, icc_profile=None)
             empty_densities.remove(density)
         except Exception as e:
             logging.warning("Invalid image file at %s: %s", last_icon_path, e)
@@ -1670,7 +1716,7 @@ def create_metadata_from_template(apk):
         with open('template.yml') as f:
             metatxt = f.read()
         if 'name' in apk and apk['name'] != '':
-            metatxt = re.sub(r'^(((Auto)?Name|Summary):).*$',
+            metatxt = re.sub(r'''^(((Auto)?Name|Summary):)[ '"\.]*$''',
                              r'\1 ' + apk['name'],
                              metatxt,
                              flags=re.IGNORECASE | re.MULTILINE)