Skip to content
Merged
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
39 changes: 29 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,23 @@ jobs:

build:
needs: prepare
runs-on: ${{ matrix.platform }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
platform: [windows-latest, macos-latest, ubuntu-22.04]
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-latest
target: aarch64-pc-windows-msvc
- os: macos-13
target: x86_64-apple-darwin
- os: macos-14
target: aarch64-apple-darwin
- os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
- os: ubuntu-22.04-arm64
target: aarch64-unknown-linux-gnu

steps:
- name: Checkout (tag)
Expand All @@ -84,14 +96,17 @@ jobs:
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable

- name: Add Rust target
run: rustup target add ${{ matrix.target }}

- name: Cache Rust
uses: swatinem/rust-cache@v2
with:
workspaces: |
src-tauri -> src-tauri/target

- name: Install Linux dependencies
if: matrix.platform == 'ubuntu-22.04'
if: startsWith(matrix.os, 'ubuntu-22.04')
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf rpm
Expand All @@ -115,26 +130,30 @@ jobs:
prerelease: false
tagName: ${{ needs.prepare.outputs.tag_name }}
releaseName: EndCat ${{ needs.prepare.outputs.tag_name }}
target: ${{ matrix.target }}

- name: Create portable exe (Windows)
if: matrix.platform == 'windows-latest'
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
$version = "${{ needs.prepare.outputs.app_version }}"
$exe = "src-tauri/target/release/endfield-cat.exe"
if (-not (Test-Path -LiteralPath $exe)) {
throw "Missing binary: $exe"
}
$candidates = @(
"src-tauri/target/${{ matrix.target }}/release/endfield-cat.exe",
"src-tauri/target/release/endfield-cat.exe"
)
$exe = $candidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
if (-not $exe) { throw "Missing binary for target ${{ matrix.target }}" }

$outDir = "src-tauri/target/release/bundle/portable"
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
$portableExe = Join-Path $outDir ("endfield-cat_{0}_x64_portable.exe" -f $version)
$suffix = if ("${{ matrix.target }}" -like "aarch64*") { "arm64" } else { "x64" }
$portableExe = Join-Path $outDir ("endfield-cat_{0}_{1}_portable.exe" -f $version, $suffix)
Copy-Item -LiteralPath $exe -Destination $portableExe -Force

"PORTABLE_EXE=$portableExe" >> $env:GITHUB_ENV

- name: Upload portable exe to release (Windows)
if: matrix.platform == 'windows-latest'
if: matrix.os == 'windows-latest'
shell: pwsh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
7 changes: 2 additions & 5 deletions src-tauri/src/app_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,17 @@ pub async fn reset_metadata(
#[tauri::command]
pub async fn update_metadata(
window: tauri::Window,
app: AppHandle,
_app: AppHandle,
client: State<'_, reqwest::Client>,
base_url: Option<String>,
) -> Result<metadata::MetadataStatus, String> {
let exe_dir = exe_dir()?;

// Use app version for metadata URL
let app_version = app.package_info().version.to_string();

metadata::update_metadata(
&exe_dir,
&client,
base_url,
Some(app_version),
None,

Choose a reason for hiding this comment

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

security-critical critical

This section of the update_metadata command is part of a flow that is vulnerable to a critical Path Traversal vulnerability. The base_url (which can be configured by the user) is passed to metadata::update_metadata, which fetches a manifest.json. The path field from this manifest is used to construct local file paths using metadata_dir.join(path) without proper sanitization. This allows an attacker to provide paths containing .. or absolute paths, leading to arbitrary file writes and potential Remote Code Execution (RCE).

Additionally, the current implementation hardcodes the metadata version to None (which resolves to latest), overriding any user-configured version set in the frontend. This means if a user sets a specific metadata version in the frontend, it will be ignored.

To remediate the Path Traversal, strict path validation must be implemented in src-tauri/src/services/metadata.rs to ensure the resulting path remains within the intended directory. For the versioning issue, the update_metadata command should be modified to accept a version parameter from the frontend and pass it to the services::metadata::update_metadata function. This will also require corresponding frontend changes in src/api/tauriCommands.ts and src/stores/app.ts.

|progress| {
let _ = window.emit("metadata-update-progress", progress);
},
Expand Down
4 changes: 2 additions & 2 deletions src/pages/SettingsPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const testSourceConnection = async (source: MetadataSourceType) => {
connectivity.value[source] = { status: 'testing', latency: 0, error: '' }
const start = performance.now()
try {
const version = appStore.currentAppVersion || metadataVersion.value
const version = metadataVersion.value.trim() || 'latest'
await fetchMetadataManifest({
baseUrl,
version
Expand Down Expand Up @@ -304,7 +304,7 @@ const verifyMetadataFiles = async () => {
const resetMetadata = async () => {
resetMetadataLoading.value = true
try {
const version = appStore.currentAppVersion || metadataVersion.value
const version = metadataVersion.value.trim() || 'latest'
await resetMetadataCommand({
baseUrl: metadataBaseUrl.value,
version
Expand Down
7 changes: 3 additions & 4 deletions src/stores/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ export const useAppStore = defineStore('app', () => {
if (sourceType === 'custom') {
return normalizeBaseUrl(customBase ?? metadataCustomBase.value)
}
// 优先使用 app version,latest 作为兜底
const version = currentAppVersion.value || metadataVersion.value.trim() || DEFAULT_METADATA_VERSION
// 默认使用 latest,可通过 metadataVersion 覆盖
const version = metadataVersion.value.trim() || DEFAULT_METADATA_VERSION
if (sourceType === 'mirror') {
return METADATA_MIRROR_TEMPLATE.replace('{version}', version)
}
Expand Down Expand Up @@ -236,7 +236,7 @@ export const useAppStore = defineStore('app', () => {

if (metadataBaseUrl.value.trim()) {
try {
const version = currentAppVersion.value || metadataVersion.value
const version = metadataVersion.value.trim() || DEFAULT_METADATA_VERSION
const remote = await fetchMetadataManifest<RemoteManifest>({ baseUrl: metadataBaseUrl.value, version })
merged = { ...status, remote }
} catch (error) {
Expand Down Expand Up @@ -326,4 +326,3 @@ export const useAppStore = defineStore('app', () => {
}
})