Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 95 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -859,16 +859,24 @@ jobs:
api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }}
api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }}

update-appcast:
needs: [set-metadata, release-finalize]
publish-update-metadata:
needs:
[
set-metadata,
upload-s3,
upload-release-artifacts,
upload-google-play,
upload-testflight,
]
if: |
always() &&
!cancelled() &&
needs.release-finalize.result == 'success' &&
(needs.set-metadata.outputs.platform == 'all' ||
contains(needs.set-metadata.outputs.platform, 'macos') ||
contains(needs.set-metadata.outputs.platform, 'windows') ||
contains(needs.set-metadata.outputs.platform, 'linux')) &&
needs.set-metadata.outputs.build_type != 'nightly'
needs.set-metadata.outputs.is_test_run != 'true' &&
needs.set-metadata.outputs.build_type != 'nightly' &&
needs.upload-s3.result == 'success' &&
needs.upload-release-artifacts.result == 'success' &&
(needs.upload-google-play.result == 'success' || needs.upload-google-play.result == 'skipped') &&
(needs.upload-testflight.result == 'success' || needs.upload-testflight.result == 'skipped')
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand All @@ -885,11 +893,67 @@ jobs:
- name: Install Python dependencies
run: python3 -m pip install -r scripts/requirements.txt

- name: Update appcast.xml
- name: Test update metadata generator
run: python3 -m unittest scripts/ci/generate_update_metadata_test.py

- name: Publish update metadata sidecars
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
GH_TOKEN: ${{ github.token }}
BUILD_TYPE: ${{ needs.set-metadata.outputs.build_type }}
RELEASE_TAG: ${{ needs.set-metadata.outputs.release_tag }}
BUCKET: ${{ vars.S3_RELEASES_BUCKET }}
run: |
# Strip 'v' prefix for S3 paths
VERSION="${RELEASE_TAG#v}"

rm -rf release-assets update-metadata
mkdir -p release-assets update-metadata

gh release download "$RELEASE_TAG" --pattern 'lantern-installer*' --dir release-assets

mapfile -t artifacts < <(
find release-assets -type f \( \
-name 'lantern-installer*.dmg' -o \
-name 'lantern-installer*.exe' -o \
-name 'lantern-installer*.apk' -o \
-name 'lantern-installer*.deb' \
\) | sort
)
if [[ "${#artifacts[@]}" -eq 0 ]]; then
echo "No updater-backed release artifacts found; skipping update metadata"
exit 0
fi

python3 scripts/ci/generate_update_metadata.py \
--build-type "$BUILD_TYPE" \
--version "$VERSION" \
--bucket "$BUCKET" \
--output-dir update-metadata \
"${artifacts[@]}"

for metadata in update-metadata/*.update.json; do
name="$(basename "$metadata")"
aws s3 cp "$metadata" "s3://${BUCKET}/releases/${BUILD_TYPE}/${VERSION}/${name}" --acl public-read
aws s3 cp "$metadata" "s3://${BUCKET}/releases/${BUILD_TYPE}/latest/${name}" --acl public-read
gh release upload "$RELEASE_TAG" "$metadata" --clobber
done

- name: Publish legacy appcast.xml
if: |
needs.set-metadata.outputs.build_type != 'nightly' &&
(
needs.set-metadata.outputs.platform == 'all' ||
contains(needs.set-metadata.outputs.platform, 'macos') ||
contains(needs.set-metadata.outputs.platform, 'windows') ||
contains(needs.set-metadata.outputs.platform, 'linux')
)
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.CI_PRIVATE_REPOS_GH_TOKEN }}
GH_TOKEN: ${{ github.token }}
BUILD_TYPE: ${{ needs.set-metadata.outputs.build_type }}
RELEASE_TAG: ${{ needs.set-metadata.outputs.release_tag }}
BUCKET: ${{ vars.S3_RELEASES_BUCKET }}
Expand All @@ -899,7 +963,8 @@ jobs:

python3 scripts/generate_appcast.py

# appcast, like installers, always upload a versioned copy alongside "latest"
# Keep this legacy feed for released clients that still point at S3.
# New clients use lantern-cloud's channel-aware appcast endpoint.
aws s3 cp appcast.xml "s3://${BUCKET}/releases/${BUILD_TYPE}/${VERSION}/appcast.xml" --acl public-read
aws s3 cp appcast.xml "s3://${BUCKET}/releases/${BUILD_TYPE}/latest/appcast.xml" --acl public-read
if [[ "$BUILD_TYPE" == "production" ]]; then
Expand All @@ -910,6 +975,22 @@ jobs:
# not git because we may be in detached HEAD
gh release upload "$RELEASE_TAG" appcast.xml --clobber

verify-update-service:
needs: [set-metadata, publish-update-metadata, release-finalize]
if: |
!cancelled() &&
needs.set-metadata.outputs.build_type == 'beta' &&
needs.publish-update-metadata.result == 'success' &&
needs.release-finalize.result == 'success'
uses: ./.github/workflows/verify-update-service.yml
with:
version: ${{ needs.set-metadata.outputs.version }}
channel: beta
platform: ${{ needs.set-metadata.outputs.platform }}
update_url: "https://update.getlantern.org/update/lantern"
timeout_seconds: 2700
interval_seconds: 60

release-finalize:
needs:
[
Expand All @@ -918,6 +999,7 @@ jobs:
upload-release-artifacts,
upload-google-play,
upload-testflight,
publish-update-metadata,
]
if: always()
runs-on: ubuntu-latest
Expand All @@ -938,6 +1020,7 @@ jobs:
env.BUILD_TYPE != 'nightly' &&
needs.upload-s3.result == 'success' &&
needs.upload-release-artifacts.result == 'success' &&
needs.publish-update-metadata.result == 'success' &&
(needs.upload-google-play.result == 'success' || needs.upload-google-play.result == 'skipped') &&
(needs.upload-testflight.result == 'success' || needs.upload-testflight.result == 'skipped')
env:
Expand All @@ -953,6 +1036,7 @@ jobs:
(env.CLEANUP_ON_FAILURE == 'true' &&
!(needs.upload-s3.result == 'success' &&
needs.upload-release-artifacts.result == 'success' &&
needs.publish-update-metadata.result == 'success' &&
(needs.upload-google-play.result == 'success' || needs.upload-google-play.result == 'skipped') &&
(needs.upload-testflight.result == 'success' || needs.upload-testflight.result == 'skipped')))
env:
Expand Down Expand Up @@ -995,6 +1079,7 @@ jobs:
env.CLEANUP_ON_FAILURE != 'true' &&
!(needs.upload-s3.result == 'success' &&
needs.upload-release-artifacts.result == 'success' &&
needs.publish-update-metadata.result == 'success' &&
(needs.upload-google-play.result == 'success' || needs.upload-google-play.result == 'skipped') &&
(needs.upload-testflight.result == 'success' || needs.upload-testflight.result == 'skipped'))
run: |
Expand Down
88 changes: 88 additions & 0 deletions .github/workflows/verify-update-service.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Verify Update Service

on:
workflow_dispatch:
inputs:
version:
description: "Release version to verify, with or without leading v"
required: true
type: string
channel:
description: "Release channel to verify"
required: false
type: choice
options:
- beta
default: beta
platform:
description: "Release platform to verify ('all' or comma-separated platforms)"
required: false
type: string
default: all
update_url:
description: "Lantern update endpoint"
required: false
type: string
default: "https://update.getlantern.org/update/lantern"
timeout_seconds:
description: "How long to poll the update service"
required: false
type: number
default: 2700
interval_seconds:
description: "Delay between polling attempts"
required: false
type: number
default: 60
workflow_call:
inputs:
version:
required: true
type: string
channel:
required: false
type: string
default: beta
platform:
required: false
type: string
default: all
update_url:
required: false
type: string
default: "https://update.getlantern.org/update/lantern"
timeout_seconds:
required: false
type: number
default: 2700
interval_seconds:
required: false
type: number
default: 60

permissions:
contents: read

concurrency:
group: verify-update-service-${{ inputs.channel }}-${{ inputs.platform }}-${{ inputs.version }}
cancel-in-progress: false

jobs:
verify:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Test update service verifier
run: python3 -m unittest scripts/ci/verify_update_service_test.py

- name: Run update service verifier
run: |
python3 scripts/ci/verify_update_service.py \
--update-url "${{ inputs.update_url }}" \
--channel "${{ inputs.channel }}" \
--platform "${{ inputs.platform }}" \
--version "${{ inputs.version }}" \
--timeout-seconds "${{ inputs.timeout_seconds }}" \
--interval-seconds "${{ inputs.interval_seconds }}"
Comment on lines +80 to +88

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Pass inputs via environment variables to prevent shell injection.

${{ inputs.* }} expressions are expanded by GitHub Actions before the shell executes. If any input (especially version, which may come from a git tag) contains shell metacharacters like $(cmd) or ; cmd, arbitrary code would execute on the runner. Exploitability is limited to users with write access, but using env vars is a standard GitHub Actions hardening practice.

🛡️ Proposed fix using environment variables
       - name: Run update service verifier
+        env:
+          UPDATE_URL: ${{ inputs.update_url }}
+          CHANNEL: ${{ inputs.channel }}
+          PLATFORM: ${{ inputs.platform }}
+          VERSION: ${{ inputs.version }}
+          TIMEOUT_SECONDS: ${{ inputs.timeout_seconds }}
+          INTERVAL_SECONDS: ${{ inputs.interval_seconds }}
         run: |
           python3 scripts/ci/verify_update_service.py \
-            --update-url "${{ inputs.update_url }}" \
-            --channel "${{ inputs.channel }}" \
-            --platform "${{ inputs.platform }}" \
-            --version "${{ inputs.version }}" \
-            --timeout-seconds "${{ inputs.timeout_seconds }}" \
-            --interval-seconds "${{ inputs.interval_seconds }}"
+            --update-url "$UPDATE_URL" \
+            --channel "$CHANNEL" \
+            --platform "$PLATFORM" \
+            --version "$VERSION" \
+            --timeout-seconds "$TIMEOUT_SECONDS" \
+            --interval-seconds "$INTERVAL_SECONDS"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Run update service verifier
run: |
python3 scripts/ci/verify_update_service.py \
--update-url "${{ inputs.update_url }}" \
--channel "${{ inputs.channel }}" \
--platform "${{ inputs.platform }}" \
--version "${{ inputs.version }}" \
--timeout-seconds "${{ inputs.timeout_seconds }}" \
--interval-seconds "${{ inputs.interval_seconds }}"
- name: Run update service verifier
env:
UPDATE_URL: ${{ inputs.update_url }}
CHANNEL: ${{ inputs.channel }}
PLATFORM: ${{ inputs.platform }}
VERSION: ${{ inputs.version }}
TIMEOUT_SECONDS: ${{ inputs.timeout_seconds }}
INTERVAL_SECONDS: ${{ inputs.interval_seconds }}
run: |
python3 scripts/ci/verify_update_service.py \
--update-url "$UPDATE_URL" \
--channel "$CHANNEL" \
--platform "$PLATFORM" \
--version "$VERSION" \
--timeout-seconds "$TIMEOUT_SECONDS" \
--interval-seconds "$INTERVAL_SECONDS"
🧰 Tools
🪛 zizmor (1.26.1)

[error] 83-83: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 84-84: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 85-85: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 86-86: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/verify-update-service.yml around lines 80 - 88, The “Run
update service verifier” step is passing GitHub Actions inputs directly into the
shell command, which can allow shell injection if a value like version contains
metacharacters. Move the inputs from the run block into environment variables on
the same step, then update the verify_update_service.py invocation to read from
those env vars instead of interpolating `${{ inputs.* }}` inside the shell
command. Keep the change localized to the workflow step that calls
scripts/ci/verify_update_service.py.

Source: Linters/SAST tools

11 changes: 5 additions & 6 deletions lib/core/common/app_urls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@ class AppUrls {
static String lanternGithub = 'https://github.com/getlantern/lantern';
static String telegramBot = 'https://t.me/lantern_official_bot';
static String unbounded = 'https://unbounded.lantern.io';
static const appcastProd =
'https://s3.amazonaws.com/lantern.io/releases/production/latest/appcast.xml';
static const appcastBeta =
'https://s3.amazonaws.com/lantern.io/releases/beta/latest/appcast.xml';
static const updateServiceLantern =
'https://update.getlantern.org/update/lantern';
static const appcastProd = '$updateServiceLantern/appcast.xml?channel=stable';
static const appcastBeta = '$updateServiceLantern/appcast.xml?channel=beta';
static String manuallyServerSetupURL =
'https://github.com/getlantern/lantern-server-manager';
static String digitalOceanBillingUrl =
'https://cloud.digitalocean.com/account/billing';
static String androidSideloadUpdateEndpoint =
'https://update.getlantern.org/update/lantern';
static String androidSideloadUpdateEndpoint = updateServiceLantern;

static String appcastFor(String buildType) {
switch (buildType) {
Expand Down
2 changes: 1 addition & 1 deletion lib/core/updater/android_sideload_updater.dart
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class AndroidSideloadUpdater {
}
if (!flags.getBool(
FeatureFlag.androidSideloadAutoUpdateEnabled,
defaultValue: false,
defaultValue: AppBuildInfo.buildType == 'beta',
)) {
if (!logDisabled) {
appLogger.info(
Expand Down
2 changes: 1 addition & 1 deletion macos/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
<key>NSUserNotificationAlertStyle</key>
<string>alert</string>
<key>SUFeedURL</key>
<string>https://s3.amazonaws.com/lantern.io/releases/production/latest/appcast.xml</string>
<string>https://update.getlantern.org/update/lantern/appcast.xml?channel=stable</string>
<key>FlutterDeepLinkingEnabled</key>
<false/>
<key>CFBundleURLTypes</key>
Expand Down
Loading
Loading