diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000000..74362ebe34 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,34 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + + - package-ecosystem: pip + directory: /backend + schedule: + interval: daily + + - package-ecosystem: pip + directory: backend/requirements + schedule: + interval: "monthly" + labels: [ ] + ignore: + - dependency-name: "*" + + - package-ecosystem: npm + directory: /client + schedule: + interval: daily + + - package-ecosystem: docker + directory: /docker + schedule: + interval: daily + + - package-ecosystem: pip + directory: /documentation + schedule: + interval: daily diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 26dc251551..dd919b228c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,12 +2,15 @@ name: Build on: [ push, pull_request ] +# Declare default permissions as read only. +permissions: read-all + jobs: run_build: runs-on: "ubuntu-latest" steps: - name: Check out repository code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 1 diff --git a/.github/workflows/codacy.yml b/.github/workflows/codacy.yml index 6cb0765ece..26d3bf45a2 100644 --- a/.github/workflows/codacy.yml +++ b/.github/workflows/codacy.yml @@ -22,8 +22,8 @@ on: schedule: - cron: '33 6 * * 2' -permissions: - contents: read +# Declare default permissions as read only. +permissions: read-all jobs: codacy-security-scan: @@ -36,7 +36,7 @@ jobs: steps: # Checkout the repository to the GitHub Actions runner - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis - name: Run Codacy Analysis CLI @@ -56,6 +56,6 @@ jobs: # Upload the SARIF file generated in the previous step - name: Upload SARIF results file - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13 with: sarif_file: results.sarif diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index bb581d821a..aa2a227ee9 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13 with: sarif_file: results.sarif diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9353271e7d..499cfb257b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,6 +2,9 @@ name: Test on: [ push, pull_request ] +# Declare default permissions as read only. +permissions: read-all + env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} @@ -10,7 +13,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: Check out repository code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 1 diff --git a/README.md b/README.md index d240196abc..64f81b4fd5 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,11 @@ The software is recognized by the [Digital Public Good Alliance](https://digital Project best practices and scores: | Metric | Score | :---: | :---: | +| [OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/) | [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3816/badge)](https://bestpractices.coreinfrastructure.org/projects/3816) +| [OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/ossf/scorecard/badge)](https://scorecard.dev/viewer/?uri=github.com/globaleaks/globaleaks-whistleblowing-software) | [MDN HTTP Observatory](https://developer.mozilla.org/en-US/observatory/analyze?host=try.globaleaks.org) | ![Status](https://img.shields.io/badge/observatory-A%2B-brightgreen) | [Security Headers](https://securityheaders.com/?q=https%3A%2F%2Ftry.globaleaks.org%2F) | ![Status](https://img.shields.io/badge/security%20headers-A%2B-brightgreen) | [SSLLabs](https://www.ssllabs.com/ssltest/analyze.html?d=try.globaleaks.org) | [![Status](https://img.shields.io/static/v1?label=SSLLabs&message=A%2B&color=%3CCOLOR%3E)](https://www.ssllabs.com/ssltest/analyze.html?d=try.globaleaks.org&latest) -| [CII Best Practices](https://bestpractices.coreinfrastructure.org/) | [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3816/badge)](https://bestpractices.coreinfrastructure.org/projects/3816) Project statistics on OpenHub: [www.openhub.net/p/globaleaks](https://www.openhub.net/p/globaleaks) diff --git a/backend/requirements/requirements-bionic.txt b/backend/requirements/requirements-bionic.txt index 45d4f7d207..187e2704bf 100644 --- a/backend/requirements/requirements-bionic.txt +++ b/backend/requirements/requirements-bionic.txt @@ -3,7 +3,7 @@ cryptography==2.6.1 h2==3.0.1 idna==2.6 priority==1.3.0 -pyopenSSL==17.5.0 +pyopenSSL==24.2.1 pynacl==1.2 pyotp==2.2.6 python_gnupg==0.4.1 diff --git a/backend/requirements/requirements-bookworm.txt b/backend/requirements/requirements-bookworm.txt index 77057c722b..6b6f062dbe 100644 --- a/backend/requirements/requirements-bookworm.txt +++ b/backend/requirements/requirements-bookworm.txt @@ -3,7 +3,7 @@ cryptography==38.0.4 h2==4.1.0 idna==3.3 priority==2.0.0 -pyopenSSL==23.0.0 +pyopenSSL==24.2.1 pynacl==1.5.0 pyotp==2.6.0 python_gnupg==0.4.9 diff --git a/backend/requirements/requirements-bullseye.txt b/backend/requirements/requirements-bullseye.txt index 06c4916b73..3194b39a13 100644 --- a/backend/requirements/requirements-bullseye.txt +++ b/backend/requirements/requirements-bullseye.txt @@ -3,7 +3,7 @@ cryptography==3.3.2 h2==4.0.0 idna==2.10 priority==1.3.0 -pyopenSSL==20.0.1 +pyopenSSL==24.2.1 pynacl==1.4.0 pyotp==2.3.0 python_gnupg==0.4.6 diff --git a/backend/requirements/requirements-buster.txt b/backend/requirements/requirements-buster.txt index 12831e0b0f..e62c55353b 100644 --- a/backend/requirements/requirements-buster.txt +++ b/backend/requirements/requirements-buster.txt @@ -3,7 +3,7 @@ cryptography==2.6.1 h2==3.0.1 idna==2.6 priority==1.3.0 -pyopenSSL==19.0.0 +pyopenSSL==24.2.1 pynacl==1.3.0 pyotp==2.2.7 python_gnupg==0.4.4 diff --git a/backend/requirements/requirements-focal.txt b/backend/requirements/requirements-focal.txt index 2523ba4422..73a913706f 100644 --- a/backend/requirements/requirements-focal.txt +++ b/backend/requirements/requirements-focal.txt @@ -3,7 +3,7 @@ cryptography==2.8 h2==3.1.1 idna==2.8 priority==1.3.0 -pyopenSSL==19.0.0 +pyopenSSL==24.2.1 pynacl==1.3.0 pyotp==2.3.0 python_gnupg==0.4.5 diff --git a/backend/requirements/requirements-jammy.txt b/backend/requirements/requirements-jammy.txt index 4b6bc66e99..dde46dce9d 100644 --- a/backend/requirements/requirements-jammy.txt +++ b/backend/requirements/requirements-jammy.txt @@ -3,7 +3,7 @@ cryptography==3.4.8 h2==4.1.0 idna==2.10 priority==1.3.0 -pyopenSSL==21.0.0 +pyopenSSL==24.2.1 pynacl==1.5.0 pyotp==2.3.0 python_gnupg==0.4.8 diff --git a/backend/requirements/requirements-noble.txt b/backend/requirements/requirements-noble.txt index a6a78f9605..044a4d1476 100644 --- a/backend/requirements/requirements-noble.txt +++ b/backend/requirements/requirements-noble.txt @@ -4,7 +4,7 @@ fpdf2==2.7.8 h2==4.1.0 idna==3.6 priority==2.0.0 -pyopenSSL==23.2.0 +pyopenSSL==24.2.1 pynacl==1.5.0 pyotp==2.9.0 python_gnupg==0.5.2 diff --git a/client/app/src/app-guard.service.ts b/client/app/src/app-guard.service.ts index 643731465a..7ccd5bc939 100644 --- a/client/app/src/app-guard.service.ts +++ b/client/app/src/app-guard.service.ts @@ -3,17 +3,18 @@ import {Router, UrlTree} from "@angular/router"; import {Observable} from "rxjs"; import {AuthenticationService} from "@app/services/helper/authentication.service"; import {AppDataService} from "@app/app-data.service"; +import {UtilsService} from "@app/shared/services/utils.service"; @Injectable({ providedIn: "root" }) export class SessionGuard { - constructor(private router: Router, private appDataService: AppDataService, public authenticationService: AuthenticationService) { + constructor(private router: Router, private appDataService: AppDataService, public authenticationService: AuthenticationService, protected utilsService: UtilsService) { } canActivate(): Observable | Promise | boolean | UrlTree { if (!this.authenticationService.session) { - this.router.navigateByUrl("/login").then(); + this.utilsService.routeGuardRedirect(); return false; } else { this.appDataService.page = this.router.url; diff --git a/client/app/src/models/authentication/session.ts b/client/app/src/models/authentication/session.ts index 7c3da163bf..2a79168c55 100644 --- a/client/app/src/models/authentication/session.ts +++ b/client/app/src/models/authentication/session.ts @@ -1,7 +1,4 @@ -import {redirectResolverModel} from "../resolvers/redirect-resolver-model"; - export class Session { - redirect: redirectResolverModel; id: string; role: string; encryption: boolean; @@ -13,6 +10,7 @@ export class Session { two_factor: boolean; permissions: { can_upload_files: boolean }; token: any; + redirect: string; } export interface Properties { diff --git a/client/app/src/services/helper/authentication.service.ts b/client/app/src/services/helper/authentication.service.ts index 1eca39fac2..b6faee93da 100644 --- a/client/app/src/services/helper/authentication.service.ts +++ b/client/app/src/services/helper/authentication.service.ts @@ -1,4 +1,4 @@ -import {Injectable} from "@angular/core"; +import {Injectable, SecurityContext} from "@angular/core"; import {LoginDataRef} from "@app/pages/auth/login/model/login-model"; import {HttpService} from "@app/shared/services/http.service"; import {Observable} from "rxjs"; @@ -10,6 +10,8 @@ import {TitleService} from "@app/shared/services/title.service"; import {HttpClient, HttpErrorResponse, HttpHeaders} from "@angular/common/http"; import {NgbModal} from "@ng-bootstrap/ng-bootstrap"; import {OtkcAccessComponent} from "@app/shared/modals/otkc-access/otkc-access.component"; +import {DomSanitizer} from '@angular/platform-browser'; + @Injectable({ providedIn: "root" @@ -21,7 +23,7 @@ export class AuthenticationService { requireAuthCode: boolean = false; loginData: LoginDataRef = new LoginDataRef(); - constructor(private http: HttpClient, private modalService: NgbModal,private titleService: TitleService, private activatedRoute: ActivatedRoute, private httpService: HttpService, private appDataService: AppDataService, private router: Router) { + constructor(private http: HttpClient, private modalService: NgbModal,private titleService: TitleService, private activatedRoute: ActivatedRoute, private httpService: HttpService, private appDataService: AppDataService, private router: Router, private sanitizer: DomSanitizer) { this.init(); } @@ -101,9 +103,11 @@ export class AuthenticationService { requestObservable.subscribe( { next: (response: Session) => { - this.reset() if (response.redirect) { - this.router.navigate([response.redirect]).then(); + response.redirect = this.sanitizer.sanitize(SecurityContext.URL, response.redirect) || ''; + if (response.redirect) { + this.router.navigate([response.redirect]).then(); + } } this.setSession(response); if (response && response && response.properties && response.properties.new_receipt) { @@ -126,36 +130,39 @@ export class AuthenticationService { }; return; } - const src = this.activatedRoute.snapshot.queryParams['src']; - if (src) { - this.router.navigate([src]).then(); - location.replace(src); + + if (this.session.role === "whistleblower") { + if (password) { + this.appDataService.receipt = password; + this.titleService.setPage("tippage"); + } else if (this.session.properties.operator_session) { + this.router.navigate(['/']); + } } else { - if (this.session.role === "whistleblower") { - if (password) { - this.appDataService.receipt = password; - this.titleService.setPage("tippage"); - } else if (this.session.properties.operator_session) { - this.router.navigate(['/']); - } - } else { - if (!callback) { - let redirect = this.activatedRoute.snapshot.queryParams['redirect'] || undefined; - this.reset(); - redirect = this.activatedRoute.snapshot.queryParams['redirect'] || '/'; - const redirectURL = decodeURIComponent(redirect); - if (redirectURL !== "/") { - this.router.navigate([redirectURL]); - } else { - this.appDataService.updateShowLoadingPanel(true); - this.router.navigate([this.session.homepage], { - queryParams: this.activatedRoute.snapshot.queryParams, - queryParamsHandling: "merge" - }).then(); - } + if (!callback) { + this.reset(); + + let redirect = this.activatedRoute.snapshot.queryParams['redirect'] || undefined; + redirect = this.activatedRoute.snapshot.queryParams['redirect'] || '/'; + redirect = decodeURIComponent(redirect); + + if (redirect !== "/") { + redirect = this.sanitizer.sanitize(SecurityContext.URL, redirect) || ''; + + // Honor only local redirects + if (redirect.startsWith("/")) { + this.router.navigate([redirect]); + } + } else { + this.appDataService.updateShowLoadingPanel(true); + this.router.navigate([this.session.homepage], { + queryParams: this.activatedRoute.snapshot.queryParams, + queryParamsHandling: "merge" + }).then(); } } } + if (callback) { callback(); } diff --git a/client/app/src/shared/guards/receiver.guard.ts b/client/app/src/shared/guards/receiver.guard.ts index 10e042b6c7..f9edc68f7a 100644 --- a/client/app/src/shared/guards/receiver.guard.ts +++ b/client/app/src/shared/guards/receiver.guard.ts @@ -16,7 +16,7 @@ export class ReceiverGuard { if (this.authenticationService.session) { if(this.authenticationService.session.role === "receiver"){ this.appConfigService.setPage(this.router.url); - }else { + } else { this.router.navigateByUrl("/login").then(); } return true; diff --git a/client/app/src/shared/pipes/strip-html.pipe.ts b/client/app/src/shared/pipes/strip-html.pipe.ts index 862056b007..3e6556e70d 100644 --- a/client/app/src/shared/pipes/strip-html.pipe.ts +++ b/client/app/src/shared/pipes/strip-html.pipe.ts @@ -1,4 +1,5 @@ import {Pipe, PipeTransform} from "@angular/core"; +import * as DOMPurify from 'dompurify'; @Pipe({ name: "stripHtml" @@ -6,6 +7,7 @@ import {Pipe, PipeTransform} from "@angular/core"; export class StripHtmlPipe implements PipeTransform { transform(value: string): string { - return value.replace(/<[^>]*>?/gm, ""); + // Use DOMPurify to sanitize input + return (DOMPurify as any).default.sanitize(value); } -} \ No newline at end of file +} diff --git a/client/package.json b/client/package.json index c9cfe9918c..1dfa045497 100644 --- a/client/package.json +++ b/client/package.json @@ -45,6 +45,7 @@ "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", "@types/angular": "1.8.9", + "@types/dompurify": "3.0.5", "@types/flowjs__flow.js": "2.13.3", "@types/lodash-es": "4.17.12", "@types/marked": "6.0.0", @@ -54,6 +55,7 @@ "angularx-qrcode": "18.0.2", "bootstrap": "5.3.3", "chart.js": "4.4.4", + "dompurify": "3.1.7", "lodash-es": "4.17.21", "ng-multiselect-dropdown": "1.0.0", "ng2-charts": "6.0.1", diff --git a/docker/Dockerfile b/docker/Dockerfile index 42f683028e..eb8823244c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:stable-slim +FROM debian:bookworm-slim@sha256:36e591f228bb9b99348f584e83f16e012c33ba5cad44ef5981a1d7c0a93eca22 RUN apt-get update -q && \ apt-get dist-upgrade -y && \