Skip to content

Potential fix for code scanning alert no. 65: Uncontrolled data used in path expression#344

Merged
intel352 merged 2 commits intomainfrom
alert-autofix-65
Mar 16, 2026
Merged

Potential fix for code scanning alert no. 65: Uncontrolled data used in path expression#344
intel352 merged 2 commits intomainfrom
alert-autofix-65

Conversation

@intel352
Copy link
Contributor

Potential fix for https://github.com/GoCodeAlone/workflow/security/code-scanning/65

In general, to fix uncontrolled-path issues when extracting archives, you should (1) normalize the entry path, (2) reject any path that attempts to escape the intended root, and (3) ensure the final destination path is still inside the trusted directory after joining and resolving it. That means you should not only inspect the archive entry string, but also verify the resolved destPath against the trusted root.

For this specific code, the best fix without changing functionality is to tighten the path validation in bundle.Import in bundle/import.go. After computing clean := filepath.Clean(hdr.Name), we should (a) ensure it is not empty, (b) reject any entry that is . or contains ".." path components, and (c) after computing destPath := filepath.Join(destDir, clean), verify that the cleaned destPath is still within the (cleaned) destDir using a prefix check that accounts for path separators. This ensures that even unusual relative paths or platform-specific separators cannot escape destDir. Once this is done, workflowPath—which is set only when clean == "workflow.yaml"—is guaranteed to be a path to a file inside destDir, making os.ReadFile(workflowPath) safe.

Concretely:

  • In bundle/import.go, extend the validation right after filepath.Clean(hdr.Name) to reject clean == ".", any clean that contains ".." as a path component, and then, after destPath := filepath.Join(destDir, clean), compute absDestDir := filepath.Clean(destDir) and absDestPath := filepath.Clean(destPath) once, and ensure absDestPath has absDestDir as a prefix followed by a path separator or equals it. If not, return an error like invalid path in bundle.
  • To avoid recomputing absDestDir for every file, compute it once near the top of Import after successfully creating destDir.
  • No changes are needed in module/api_v1_handler.go; we only make Import strictly confine extracted paths to destDir, which keeps existing behavior (extracting into that workspace) while hardening against traversal.

No new external dependencies are required; we can use strings.HasPrefix and os.PathSeparator from the standard library.


Suggested fixes powered by Copilot Autofix. Review carefully before merging.

…in path expression

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 16, 2026 18:24
@intel352 intel352 marked this pull request as ready for review March 16, 2026 18:25
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hardens bundle.Import archive extraction to address a code scanning alert about uncontrolled data in path expressions by tightening path traversal and destination confinement checks during tar.gz import.

Changes:

  • Normalize the destination directory once and use it as the basis for path joining.
  • Add stricter validation for cleaned tar entry paths (reject absolute paths and parent directory references).
  • Verify the final computed destination path remains within the intended destination directory before writing.

Comment on lines 24 to 29
if err := os.MkdirAll(destDir, 0750); err != nil {
return nil, "", fmt.Errorf("create dest dir: %w", err)
}
// Normalise the destination directory once for safe prefix checks.
absDestDir := filepath.Clean(destDir)

@intel352
Copy link
Contributor Author

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link
Contributor

Copilot AI commented Mar 16, 2026

@intel352 I've opened a new pull request, #345, to work on those changes. Once the pull request is ready, I'll request review from you.

…ment check (#345)

* Initial plan

* Fix bundle.Import: skip dot entries, use filepath.Abs for destDir

Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
@github-actions
Copy link

⏱ Benchmark Results

No significant performance regressions detected.

benchstat comparison (baseline → PR)
## benchstat: baseline → PR
baseline-bench.txt:244: parsing iteration count: invalid syntax
baseline-bench.txt:306449: parsing iteration count: invalid syntax
baseline-bench.txt:609285: parsing iteration count: invalid syntax
baseline-bench.txt:915718: parsing iteration count: invalid syntax
baseline-bench.txt:1202684: parsing iteration count: invalid syntax
baseline-bench.txt:1503297: parsing iteration count: invalid syntax
benchmark-results.txt:244: parsing iteration count: invalid syntax
benchmark-results.txt:318785: parsing iteration count: invalid syntax
benchmark-results.txt:623703: parsing iteration count: invalid syntax
benchmark-results.txt:921112: parsing iteration count: invalid syntax
benchmark-results.txt:1218413: parsing iteration count: invalid syntax
benchmark-results.txt:1512963: parsing iteration count: invalid syntax
goos: linux
goarch: amd64
pkg: github.com/GoCodeAlone/workflow/dynamic
cpu: AMD EPYC 7763 64-Core Processor                
                            │ baseline-bench.txt │       benchmark-results.txt        │
                            │       sec/op       │    sec/op     vs base              │
InterpreterCreation-4               8.804m ± 65%   8.647m ± 65%       ~ (p=0.937 n=6)
ComponentLoad-4                     3.647m ± 10%   3.461m ±  0%  -5.10% (p=0.002 n=6)
ComponentExecute-4                  2.019µ ±  1%   2.000µ ±  0%  -0.97% (p=0.002 n=6)
PoolContention/workers-1-4          1.113µ ±  3%   1.090µ ±  1%  -2.02% (p=0.002 n=6)
PoolContention/workers-2-4          1.121µ ±  1%   1.088µ ±  3%  -2.94% (p=0.009 n=6)
PoolContention/workers-4-4          1.121µ ±  1%   1.084µ ±  1%  -3.30% (p=0.002 n=6)
PoolContention/workers-8-4          1.120µ ±  1%   1.092µ ±  1%  -2.50% (p=0.002 n=6)
PoolContention/workers-16-4         1.126µ ±  1%   1.090µ ±  2%  -3.20% (p=0.002 n=6)
ComponentLifecycle-4                3.737m ±  8%   3.475m ±  2%  -7.00% (p=0.002 n=6)
SourceValidation-4                  2.341µ ±  2%   2.275µ ±  1%  -2.82% (p=0.002 n=6)
RegistryConcurrent-4                809.9n ±  5%   766.6n ±  2%  -5.35% (p=0.004 n=6)
LoaderLoadFromString-4              3.665m ±  1%   3.542m ±  1%  -3.34% (p=0.002 n=6)
geomean                             19.47µ         18.81µ        -3.37%

                            │ baseline-bench.txt │        benchmark-results.txt         │
                            │        B/op        │     B/op      vs base                │
InterpreterCreation-4               1.944Mi ± 0%   1.944Mi ± 0%       ~ (p=0.734 n=6)
ComponentLoad-4                     2.097Mi ± 0%   2.097Mi ± 0%       ~ (p=0.558 n=6)
ComponentExecute-4                  1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4         1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                2.099Mi ± 0%   2.099Mi ± 0%       ~ (p=0.128 n=6)
SourceValidation-4                  1.984Ki ± 0%   1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                1.133Ki ± 0%   1.133Ki ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4              2.099Mi ± 0%   2.099Mi ± 0%       ~ (p=0.667 n=6)
geomean                             15.05Ki        15.05Ki       +0.00%
¹ all samples are equal

                            │ baseline-bench.txt │        benchmark-results.txt        │
                            │     allocs/op      │  allocs/op   vs base                │
InterpreterCreation-4                15.09k ± 0%   15.09k ± 0%       ~ (p=1.000 n=6)
ComponentLoad-4                      17.43k ± 0%   17.43k ± 0%       ~ (p=1.000 n=6)
ComponentExecute-4                    25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4           25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                 17.48k ± 0%   17.48k ± 0%       ~ (p=1.000 n=6) ¹
SourceValidation-4                    32.00 ± 0%    32.00 ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                  2.000 ± 0%    2.000 ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4               17.47k ± 0%   17.47k ± 0%       ~ (p=1.000 n=6) ¹
geomean                               181.2         181.2       +0.00%
¹ all samples are equal

pkg: github.com/GoCodeAlone/workflow/middleware
                                  │ baseline-bench.txt │       benchmark-results.txt       │
                                  │       sec/op       │   sec/op     vs base              │
CircuitBreakerDetection-4                  287.9n ± 5%   287.5n ± 4%       ~ (p=0.515 n=6)
CircuitBreakerExecution_Success-4          22.50n ± 0%   22.50n ± 0%       ~ (p=0.922 n=6)
CircuitBreakerExecution_Failure-4          64.55n ± 0%   64.47n ± 0%       ~ (p=0.071 n=6)
geomean                                    74.79n        74.71n       -0.10%

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │        B/op        │    B/op     vs base                │
CircuitBreakerDetection-4                 144.0 ± 0%     144.0 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │     allocs/op      │ allocs/op   vs base                │
CircuitBreakerDetection-4                 1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/module
                                 │ baseline-bench.txt │       benchmark-results.txt        │
                                 │       sec/op       │    sec/op     vs base              │
JQTransform_Simple-4                     987.1n ± 20%   889.3n ± 26%       ~ (p=0.258 n=6)
JQTransform_ObjectConstruction-4         1.521µ ±  1%   1.466µ ±  0%  -3.65% (p=0.002 n=6)
JQTransform_ArraySelect-4                3.542µ ±  1%   3.358µ ±  1%  -5.21% (p=0.002 n=6)
JQTransform_Complex-4                    39.86µ ±  1%   37.95µ ±  0%  -4.81% (p=0.002 n=6)
JQTransform_Throughput-4                 1.847µ ±  1%   1.831µ ±  1%  -0.89% (p=0.017 n=6)
SSEPublishDelivery-4                     65.42n ±  0%   64.08n ±  1%  -2.06% (p=0.002 n=6)
geomean                                  1.717µ         1.640µ        -4.46%

                                 │ baseline-bench.txt │        benchmark-results.txt         │
                                 │        B/op        │     B/op      vs base                │
JQTransform_Simple-4                   1.273Ki ± 0%     1.273Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4       1.773Ki ± 0%     1.773Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4              2.625Ki ± 0%     2.625Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                  16.22Ki ± 0%     16.22Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4               1.984Ki ± 0%     1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²                 +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                 │ baseline-bench.txt │       benchmark-results.txt        │
                                 │     allocs/op      │ allocs/op   vs base                │
JQTransform_Simple-4                     10.00 ± 0%     10.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4         15.00 ± 0%     15.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4                30.00 ± 0%     30.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                    324.0 ± 0%     324.0 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4                 17.00 ± 0%     17.00 ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/schema
                                    │ baseline-bench.txt │       benchmark-results.txt       │
                                    │       sec/op       │   sec/op     vs base              │
SchemaValidation_Simple-4                    1.101µ ± 7%   1.116µ ± 2%       ~ (p=0.788 n=6)
SchemaValidation_AllFields-4                 1.732µ ± 4%   1.694µ ± 5%       ~ (p=0.310 n=6)
SchemaValidation_FormatValidation-4          1.604µ ± 2%   1.603µ ± 1%       ~ (p=0.907 n=6)
SchemaValidation_ManySchemas-4               1.851µ ± 3%   1.809µ ± 5%       ~ (p=0.699 n=6)
geomean                                      1.542µ        1.530µ       -0.82%

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │        B/op        │    B/op     vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │     allocs/op      │ allocs/op   vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/store
                                   │ baseline-bench.txt │       benchmark-results.txt        │
                                   │       sec/op       │    sec/op     vs base              │
EventStoreAppend_InMemory-4                1.288µ ± 11%   1.195µ ± 15%       ~ (p=0.310 n=6)
EventStoreAppend_SQLite-4                  1.807m ± 15%   1.660m ±  8%  -8.09% (p=0.002 n=6)
GetTimeline_InMemory/events-10-4           14.65µ ±  2%   14.06µ ±  6%  -3.98% (p=0.009 n=6)
GetTimeline_InMemory/events-50-4           64.50µ ± 27%   62.29µ ±  1%  -3.42% (p=0.002 n=6)
GetTimeline_InMemory/events-100-4          126.9µ ±  1%   124.9µ ±  1%  -1.62% (p=0.009 n=6)
GetTimeline_InMemory/events-500-4          657.6µ ±  1%   638.7µ ±  0%  -2.88% (p=0.002 n=6)
GetTimeline_InMemory/events-1000-4         1.349m ±  1%   1.306m ±  1%  -3.21% (p=0.002 n=6)
GetTimeline_SQLite/events-10-4             114.2µ ±  1%   108.3µ ±  1%  -5.15% (p=0.002 n=6)
GetTimeline_SQLite/events-50-4             262.0µ ±  1%   250.4µ ±  1%  -4.42% (p=0.002 n=6)
GetTimeline_SQLite/events-100-4            438.8µ ±  0%   423.5µ ±  2%  -3.49% (p=0.002 n=6)
GetTimeline_SQLite/events-500-4            1.853m ±  1%   1.828m ±  1%  -1.36% (p=0.002 n=6)
GetTimeline_SQLite/events-1000-4           3.655m ±  1%   3.525m ±  1%  -3.55% (p=0.002 n=6)
geomean                                    230.1µ         220.8µ        -4.06%

                                   │ baseline-bench.txt │         benchmark-results.txt         │
                                   │        B/op        │     B/op       vs base                │
EventStoreAppend_InMemory-4                  831.0 ± 8%     758.5 ± 13%       ~ (p=0.067 n=6)
EventStoreAppend_SQLite-4                  1.984Ki ± 3%   1.983Ki ±  1%       ~ (p=0.846 n=6)
GetTimeline_InMemory/events-10-4           7.953Ki ± 0%   7.953Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4           46.62Ki ± 0%   46.62Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4          94.48Ki ± 0%   94.48Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4          472.8Ki ± 0%   472.8Ki ±  0%       ~ (p=0.242 n=6)
GetTimeline_InMemory/events-1000-4         944.3Ki ± 0%   944.3Ki ±  0%       ~ (p=0.102 n=6)
GetTimeline_SQLite/events-10-4             16.74Ki ± 0%   16.74Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4             87.14Ki ± 0%   87.14Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4            175.4Ki ± 0%   175.4Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4            846.1Ki ± 0%   846.1Ki ±  0%       ~ (p=1.000 n=6)
GetTimeline_SQLite/events-1000-4           1.639Mi ± 0%   1.639Mi ±  0%  -0.00% (p=0.015 n=6)
geomean                                    67.63Ki        67.11Ki        -0.76%
¹ all samples are equal

                                   │ baseline-bench.txt │        benchmark-results.txt        │
                                   │     allocs/op      │  allocs/op   vs base                │
EventStoreAppend_InMemory-4                  7.000 ± 0%    7.000 ± 0%       ~ (p=1.000 n=6) ¹
EventStoreAppend_SQLite-4                    53.00 ± 0%    53.00 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-10-4             125.0 ± 0%    125.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4             653.0 ± 0%    653.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4           1.306k ± 0%   1.306k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4           6.514k ± 0%   6.514k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-1000-4          13.02k ± 0%   13.02k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-10-4               382.0 ± 0%    382.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4              1.852k ± 0%   1.852k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4             3.681k ± 0%   3.681k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4             18.54k ± 0%   18.54k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-1000-4            37.29k ± 0%   37.29k ± 0%       ~ (p=1.000 n=6) ¹
geomean                                     1.162k        1.162k       +0.00%
¹ all samples are equal

Benchmarks run with go test -bench=. -benchmem -count=6.
Regressions ≥ 20% are flagged. Results compared via benchstat.

@intel352 intel352 merged commit a049138 into main Mar 16, 2026
15 checks passed
@intel352 intel352 deleted the alert-autofix-65 branch March 16, 2026 21:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants