Fixes security issue: potential remote Path Traversal via File Upload
This commit is contained in:
@@ -163,6 +163,41 @@ export class AdminMobileBuildsTabComponent implements OnInit {
|
||||
return 'iOS requires both certificate and provisioning profile files.';
|
||||
}
|
||||
|
||||
// Validate file extensions before hitting the backend
|
||||
const extChecks: Array<{ file: File | null; label: string; exts: string[] }> = [];
|
||||
if (this.buildAndroid) {
|
||||
extChecks.push({
|
||||
file: this.androidKeystoreFile,
|
||||
label: 'Android keystore',
|
||||
exts: ['.jks', '.keystore', '.p12'],
|
||||
});
|
||||
extChecks.push({
|
||||
file: this.androidDescriptorFile,
|
||||
label: 'Android descriptor',
|
||||
exts: ['.json'],
|
||||
});
|
||||
}
|
||||
if (this.buildIos) {
|
||||
extChecks.push({
|
||||
file: this.iosCertificateFile,
|
||||
label: 'iOS certificate',
|
||||
exts: ['.p12'],
|
||||
});
|
||||
extChecks.push({
|
||||
file: this.iosProfileFile,
|
||||
label: 'iOS provisioning profile',
|
||||
exts: ['.mobileprovision'],
|
||||
});
|
||||
}
|
||||
|
||||
for (const { file, label, exts } of extChecks) {
|
||||
if (!file) continue;
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
if (!exts.includes(ext)) {
|
||||
return `${label} must have one of: ${exts.join(', ')}`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -176,6 +211,7 @@ export class AdminMobileBuildsTabComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.mobileBuildBusy = true;
|
||||
let createdRequestId: number | null = null;
|
||||
try {
|
||||
const request = await firstValueFrom(this.mobileBuildService.createRequest({
|
||||
platform_android: this.buildAndroid,
|
||||
@@ -186,6 +222,7 @@ export class AdminMobileBuildsTabComponent implements OnInit {
|
||||
build_number: this.buildNumber.trim(),
|
||||
release_profile: this.buildReleaseProfile.trim() || 'release',
|
||||
}));
|
||||
createdRequestId = request.id;
|
||||
|
||||
if (this.buildAndroid && this.androidKeystoreFile && this.androidDescriptorFile) {
|
||||
await firstValueFrom(this.mobileBuildService.uploadFile(request.id, 'android', 'keystore', this.androidKeystoreFile));
|
||||
@@ -201,6 +238,13 @@ export class AdminMobileBuildsTabComponent implements OnInit {
|
||||
await this.reloadMobileBuilds();
|
||||
this.onMobileBuildSelected(request.id);
|
||||
} catch (err) {
|
||||
// Clean up orphaned request if files couldn't all be uploaded
|
||||
if (createdRequestId !== null) {
|
||||
await firstValueFrom(
|
||||
this.mobileBuildService.deleteRequest(createdRequestId),
|
||||
).catch(() => {}); // best-effort cleanup
|
||||
await this.reloadMobileBuilds().catch(() => {});
|
||||
}
|
||||
this.mobileBuildErrorMessage = this.getErrorMessage(err, 'Failed to create build request.');
|
||||
} finally {
|
||||
this.mobileBuildBusy = false;
|
||||
|
||||
@@ -8,16 +8,6 @@ export interface MobileBuildDefaults {
|
||||
stream_metadata_url: string;
|
||||
}
|
||||
|
||||
export interface MobileBuildUploadedFile {
|
||||
id: number;
|
||||
request_id: number;
|
||||
platform: 'android' | 'ios';
|
||||
file_kind: string;
|
||||
original_name: string;
|
||||
size: number;
|
||||
uploaded_at: string;
|
||||
}
|
||||
|
||||
export interface MobileBuildArtifact {
|
||||
id: number;
|
||||
request_id: number;
|
||||
@@ -49,7 +39,6 @@ export interface MobileBuildRequest {
|
||||
updated_at: string;
|
||||
started_at: string | null;
|
||||
completed_at: string | null;
|
||||
files: MobileBuildUploadedFile[];
|
||||
artifacts: MobileBuildArtifact[];
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
MobileBuildDefaults,
|
||||
MobileBuildPreflightResult,
|
||||
MobileBuildRequest,
|
||||
MobileBuildUploadedFile,
|
||||
ThemeStatus,
|
||||
} from '../interfaces/mobile-build';
|
||||
|
||||
@@ -35,19 +34,23 @@ export class MobileBuildService {
|
||||
return this.http.post<MobileBuildRequest>(`${this.baseUrl}/requests`, payload);
|
||||
}
|
||||
|
||||
deleteRequest(id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.baseUrl}/requests/${id}`);
|
||||
}
|
||||
|
||||
uploadFile(
|
||||
requestId: number,
|
||||
platform: 'android' | 'ios',
|
||||
fileKind: string,
|
||||
file: File,
|
||||
): Observable<MobileBuildUploadedFile> {
|
||||
): Observable<{ platform: string; file_kind: string; display_name: string; sha256: string; size: number }> {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
const params = new HttpParams()
|
||||
.set('platform', platform)
|
||||
.set('file_kind', fileKind);
|
||||
|
||||
return this.http.post<MobileBuildUploadedFile>(
|
||||
return this.http.post<{ platform: string; file_kind: string; display_name: string; sha256: string; size: number }>(
|
||||
`${this.baseUrl}/requests/${requestId}/files`,
|
||||
form,
|
||||
{ params },
|
||||
|
||||
Reference in New Issue
Block a user