Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions .github/workflows/build_docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ on:
description: "The tag of the client image that was built"
value: ${{ jobs.build-client.outputs.client_image_tag }}

permissions:
contents: read
packages: write

jobs:
build-server:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/deploy_docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ on:
default: "latest"
type: string

permissions:
contents: read

jobs:
deploy:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ concurrency:
group: deploy-dev-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read
packages: write

jobs:
run-tests:
uses: ./.github/workflows/run_tests.yml
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/e2e_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ name: E2E Tests
on:
workflow_call:

permissions:
contents: read

jobs:
e2e:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ concurrency:
group: deploy-prod
cancel-in-progress: false

permissions:
contents: read
packages: write

jobs:
run-tests:
uses: ./.github/workflows/run_tests.yml
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ name: Run Tests
on:
workflow_call:

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion client/src/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function formatUser(user: IMinimalUser, options: Partial<IFormatUserOptio
}

export function wordsToFilename(words: string) {
return words.replace(' ', ' ')
return words.replace(/ /g, '_')
}

export function formatUserFilename(user: ILightUser): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,13 @@ public CalendarController(ThesisPresentationService thesisPresentationService,
public ResponseEntity<String> getCalendar(@PathVariable(required = false) String researchGroupAbbreviation) {
ResearchGroup researchGroup = researchGroupService.findByAbbreviation(researchGroupAbbreviation);
if (researchGroup == null) {
log.error("Research group with abbreviation '{}' not found", researchGroupAbbreviation);
return ResponseEntity.status(404).body("Research group with abbreviation '" + researchGroupAbbreviation + "' not found");
String safeAbbr = researchGroupAbbreviation != null
? researchGroupAbbreviation.replaceAll("[\\r\\n]", "_")
: "<null>";
log.error("Research group with abbreviation '{}' not found", safeAbbr);
return ResponseEntity.status(404)
.contentType(MediaType.TEXT_PLAIN)
.body("Research group not found");
}

return ResponseEntity.ok()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import de.tum.cit.aet.thesis.constants.UploadFileType;
import de.tum.cit.aet.thesis.exception.UploadException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -22,6 +23,7 @@
import java.util.Set;

/** Handles file uploads and retrieval, including size and type validation and content-based hashing. */
@Slf4j
@Service
public class UploadService {
private final Path rootLocation;
Expand All @@ -33,7 +35,7 @@ public class UploadService {
*/
@Autowired
public UploadService(@Value("${thesis-management.storage.upload-location}") String uploadLocation) {
this.rootLocation = Path.of(uploadLocation);
this.rootLocation = Path.of(uploadLocation).toAbsolutePath().normalize();

File uploadDirectory = rootLocation.toFile();

Expand Down Expand Up @@ -84,13 +86,14 @@ public String store(MultipartFile file, Integer maxSize, UploadFileType type) {
}

String filename = StringUtils.cleanPath(computeFileHash(file) + "." + extension);
Path target = rootLocation.resolve(filename).normalize();

if (filename.contains("..")) {
throw new UploadException("Cannot store file with relative path outside current directory");
if (!target.startsWith(rootLocation)) {
throw new UploadException("Cannot store file outside upload directory");
}

try (InputStream inputStream = file.getInputStream()) {
Files.copy(inputStream, rootLocation.resolve(filename), StandardCopyOption.REPLACE_EXISTING);
Files.copy(inputStream, target, StandardCopyOption.REPLACE_EXISTING);

return filename;
}
Expand All @@ -107,11 +110,13 @@ public String store(MultipartFile file, Integer maxSize, UploadFileType type) {
*/
public FileSystemResource load(String filename) {
try {
if (filename.contains("..")) {
throw new UploadException("Cannot load file with relative path outside current directory");
Path resolved = rootLocation.resolve(filename).normalize();

if (!resolved.startsWith(rootLocation)) {
throw new UploadException("Cannot load file outside upload directory");
}

FileSystemResource file = new FileSystemResource(rootLocation.resolve(filename));
FileSystemResource file = new FileSystemResource(resolved);

file.contentLength();

Expand All @@ -121,6 +126,68 @@ public FileSystemResource load(String filename) {
}
}

/**
* Stores raw bytes as a file with the given extension, returning the content-hashed filename.
*
* @param bytes the file content
* @param extension the file extension (e.g. "png")
* @param maxSize the maximum allowed size in bytes
* @return the content-hashed filename
*/
public String storeBytes(byte[] bytes, String extension, int maxSize) {
try {
if (bytes == null || bytes.length == 0) {
throw new UploadException("Failed to store empty file");
}

if (bytes.length > maxSize) {
throw new UploadException("File size exceeds the maximum allowed size");
}

if (extension == null || !extension.matches("[a-zA-Z0-9]+")) {
throw new UploadException("Invalid file extension");
}

MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(bytes);
String hash = HexFormat.of().formatHex(hashBytes);
String filename = hash + "." + extension;

Path target = rootLocation.resolve(filename).normalize();

if (!target.startsWith(rootLocation)) {
throw new UploadException("Cannot store file outside upload directory");
}

Files.write(target, bytes);
return filename;
} catch (IOException | NoSuchAlgorithmException e) {
throw new UploadException("Failed to store file", e);
}
}

/**
* Deletes a file from the upload directory.
*
* @param filename the name of the file to delete
*/
public void deleteFile(String filename) {
if (filename == null || filename.isBlank()) {
return;
}
try {
Path target = rootLocation.resolve(filename).normalize();

if (!target.startsWith(rootLocation)) {
return;
}

Files.deleteIfExists(target);
} catch (IOException e) {
log.warn("Failed to delete file '{}': {}", filename, e.getMessage());
}
}

private String computeFileHash(MultipartFile file) throws IOException, NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (InputStream inputStream = file.getInputStream()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ void load_ValidFile_ReturnsResource() {
void load_PathTraversal_ThrowsException() {
assertThatThrownBy(() -> uploadService.load("../../../etc/passwd"))
.isInstanceOf(UploadException.class)
.hasMessageContaining("relative path");
.hasMessageContaining("outside upload directory");
}

@Test
Expand Down
Loading