Merge branch 'fix-symlinks' into 'master'

Fix symlink handling around triple-t/fastlane metadata

Closes #1358

See merge request fdroid/fdroidserver!1858
This commit is contained in:
Michael Pöhn committed 2026-08-27 08:23:20 +00:00
commit 58efbdfb50
2 files changed
+79 -2

No files matched your search

+33 -2
View File
@@ -1093,6 +1093,34 @@ def insert_funding_yml_donation_links(apps):
break
def safe_walk(top, followLinksTo):
"""Run os.walk, only following symlinks that stay within followLinksTo.
Entries whose symlink target escapes the followLinksTo directory
are dropped with a warning, so symlinks in an app's source repo
cannot leak files from the build host into the published index.
"""
followLinksTo = os.path.realpath(followLinksTo)
def _filter(root, names):
kept = []
for name in names:
target = os.path.realpath(os.path.join(root, name))
if target == followLinksTo or target.startswith(followLinksTo + os.sep):
kept.append(name)
else:
logging.warning(
_('Ignoring "{path}": symlink points outside the app source')
.format(path=os.path.join(root, name))
)
return kept
for root, dirs, files in os.walk(top, followlinks=True):
dirs[:] = _filter(root, dirs)
files[:] = _filter(root, files)
yield root, dirs, files
def copy_triple_t_store_metadata(apps):
"""Include store metadata from the app's source repo.
@@ -1162,9 +1190,10 @@ def copy_triple_t_store_metadata(apps):
# Flutter-style android subdir
gradle_subdirs.update(glob.glob(os.path.join('build', packageName, 'android', 'app', 'src', '*', 'play')))
checkout = os.path.join('build', packageName)
for d in sorted(gradle_subdirs):
logging.debug('Triple-T Gradle Play Publisher: ' + d)
for root, dirs, files in os.walk(d):
for root, dirs, files in safe_walk(d, followLinksTo=checkout):
segments = root.split('/')
if segments[-2] == 'listings' or segments[-2] == 'release-notes':
locale = segments[-1]
@@ -1262,7 +1291,9 @@ def insert_localized_app_metadata(apps):
for srcd in sorted(sourcedirs):
if not os.path.isdir(srcd):
continue
for root, dirs, files in os.walk(srcd):
topdir, packageName = srcd.split('/')[:2]
checkout = os.path.join(topdir, packageName)
for root, dirs, files in safe_walk(srcd, followLinksTo=checkout):
segments = root.split('/')
packageName = segments[1]
if packageName not in apps:
+46
View File
@@ -653,6 +653,52 @@ class UpdateTest(SetUpTearDownMixin, unittest.TestCase):
self.assertEqual(app['localized']['en-US']['name'], 'Goguma')
self.assertEqual(app['localized']['en-US']['summary'], 'An IRC client for mobile devices')
def _triple_t_app(self, packageName):
"""Write minimal metadata and return the Triple-T play dir."""
os.chdir(self.testdir)
Path('metadata').mkdir(exist_ok=True)
Path('metadata', packageName + '.yml').write_text(
'CurrentVersionCode: 1\n'
'Builds:\n - versionCode: 1\n gradle:\n - yes\n'
)
play = Path('build', packageName, 'src', 'main', 'play')
play.mkdir(parents=True)
return play
def _copy_triple_t(self, packageName):
config = dict()
fdroidserver.common.fill_config_defaults(config)
fdroidserver.common.config = config
fdroidserver.update.config = config
apps = fdroidserver.metadata.read_metadata()
fdroidserver.update.copy_triple_t_store_metadata(apps)
return apps[packageName].get('localized', {}).get('en-US', {})
@unittest.skipUnless(hasattr(os, 'symlink'), 'requires symlink support')
def test_insert_triple_t_symlink_within_checkout(self):
"""A listings dir symlinked inside the checkout is still read."""
packageName = 'com.example.app'
play = self._triple_t_app(packageName)
target = Path('build', packageName, 'store-metadata', 'listings', 'en-US')
target.mkdir(parents=True)
(target / 'full-description.txt').write_text('good description')
os.symlink(os.path.relpath(target.parent, play), play / 'listings')
self.assertEqual(self._copy_triple_t(packageName).get('description'),
'good description')
@unittest.skipUnless(hasattr(os, 'symlink'), 'requires symlink support')
def test_insert_triple_t_symlink_outside_checkout(self):
"""A listing symlinked outside the checkout must not be published."""
packageName = 'com.example.app'
play = self._triple_t_app(packageName)
en_US = play / 'listings' / 'en-US'
en_US.mkdir(parents=True)
secret = Path('secret.txt')
secret.write_text('SECRET')
os.symlink(os.path.abspath(secret), en_US / 'full-description.txt')
self.assertNotIn('SECRET',
self._copy_triple_t(packageName).get('description', ''))
def testBadGetsig(self):
"""getsig() should still be able to fetch the fingerprint of bad signatures"""
# config needed to use jarsigner and keytool