2018-03-08 18:23:27 -05:00
// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rules
import (
"go/ast"
"go/types"
2020-04-01 22:18:39 +02:00
"github.com/securego/gosec/v2"
2023-02-15 20:44:13 +01:00
"github.com/securego/gosec/v2/issue"
2018-03-08 18:23:27 -05:00
)
type readfile struct {
2023-02-15 20:44:13 +01:00
issue . MetaData
2018-07-19 18:42:25 +02:00
gosec . CallList
2025-09-12 19:01:24 +05:30
pathJoin gosec . CallList
clean gosec . CallList
2025-09-11 23:47:46 +05:30
// cleanedVar maps the declaration node of an identifier to the Clean() call node
2023-01-09 16:26:20 +01:00
cleanedVar map [ any ] ast . Node
2025-09-11 23:47:46 +05:30
// joinedVar maps the declaration node of an identifier to the Join() call node
joinedVar map [ any ] ast . Node
2018-03-08 18:23:27 -05:00
}
2018-03-09 11:27:41 +10:00
// ID returns the identifier for this rule
func ( r * readfile ) ID ( ) string {
return r . MetaData . ID
}
2018-08-27 21:34:07 -07:00
// isJoinFunc checks if there is a filepath.Join or other join function
func ( r * readfile ) isJoinFunc ( n ast . Node , c * gosec . Context ) bool {
2020-01-28 14:11:00 +01:00
if call := r . pathJoin . ContainsPkgCallExpr ( n , c , false ) ; call != nil {
2018-08-27 21:34:07 -07:00
for _ , arg := range call . Args {
// edge case: check if one of the args is a BinaryExpr
if binExp , ok := arg . ( * ast . BinaryExpr ) ; ok {
2018-10-11 15:45:31 +03:00
// iterate and resolve all found identities from the BinaryExpr
2018-08-27 21:34:07 -07:00
if _ , ok := gosec . FindVarIdentities ( binExp , c ) ; ok {
return true
}
}
2018-09-25 00:40:05 -07:00
// try and resolve identity
if ident , ok := arg . ( * ast . Ident ) ; ok {
obj := c . Info . ObjectOf ( ident )
if _ , ok := obj . ( * types . Var ) ; ok && ! gosec . TryResolve ( ident , c ) {
return true
}
2018-08-27 21:34:07 -07:00
}
}
}
return false
}
2023-01-09 16:26:20 +01:00
// isFilepathClean checks if there is a filepath.Clean for given variable
2023-01-09 16:49:02 +01:00
func ( r * readfile ) isFilepathClean ( n * ast . Ident , c * gosec . Context ) bool {
2025-09-11 23:47:46 +05:30
// quick lookup: was this var's declaration recorded as a Clean() call?
2023-01-09 16:26:20 +01:00
if _ , ok := r . cleanedVar [ n . Obj . Decl ] ; ok {
return true
2020-08-17 21:05:28 +02:00
}
2023-01-09 16:49:02 +01:00
if n . Obj . Kind != ast . Var {
return false
}
if node , ok := n . Obj . Decl . ( * ast . AssignStmt ) ; ok {
if call , ok := node . Rhs [ 0 ] . ( * ast . CallExpr ) ; ok {
if clean := r . clean . ContainsPkgCallExpr ( call , c , false ) ; clean != nil {
return true
}
}
}
2023-01-09 16:26:20 +01:00
return false
}
// trackFilepathClean tracks back the declaration of variable from filepath.Clean argument
func ( r * readfile ) trackFilepathClean ( n ast . Node ) {
if clean , ok := n . ( * ast . CallExpr ) ; ok && len ( clean . Args ) > 0 {
if ident , ok := clean . Args [ 0 ] . ( * ast . Ident ) ; ok {
2023-03-08 08:42:45 -05:00
// ident.Obj may be nil if the referenced declaration is in another file. It also may be incorrect.
// if it is nil, do not follow it.
if ident . Obj != nil {
r . cleanedVar [ ident . Obj . Decl ] = n
}
2020-08-17 21:05:28 +02:00
}
}
}
2025-09-11 23:47:46 +05:30
// trackJoinAssignStmt tracks assignments where RHS is a Join(...) call and LHS is an identifier
func ( r * readfile ) trackJoinAssignStmt ( node * ast . AssignStmt , c * gosec . Context ) {
if len ( node . Rhs ) == 0 {
return
}
if call , ok := node . Rhs [ 0 ] . ( * ast . CallExpr ) ; ok {
if r . pathJoin . ContainsPkgCallExpr ( call , c , false ) != nil {
// LHS must be an identifier (simple case)
if len ( node . Lhs ) > 0 {
if ident , ok := node . Lhs [ 0 ] . ( * ast . Ident ) ; ok && ident . Obj != nil {
r . joinedVar [ ident . Obj . Decl ] = call
}
}
}
}
}
2025-09-16 02:12:05 +05:30
// osRootSuggestion returns an Autofix suggesting the use of os.Root where supported
// to constrain file access under a fixed directory and mitigate traversal risks.
func ( r * readfile ) osRootSuggestion ( ) string {
major , minor , _ := gosec . GoVersion ( )
if major == 1 && minor >= 24 {
return "Consider using os.Root to scope file access under a fixed root (Go >=1.24). Prefer root.Open/root.Stat over os.Open/os.Stat to prevent directory traversal."
}
return ""
}
2025-09-11 23:47:46 +05:30
// isSafeJoin checks if path is baseDir + filepath.Clean(fn) joined.
// improvements over earlier naive version:
// - allow baseDir as a BasicLit or as an identifier that resolves to a string constant
// - accept Clean(...) being either a CallExpr or an identifier previously recorded as Clean result
func ( r * readfile ) isSafeJoin ( call * ast . CallExpr , c * gosec . Context ) bool {
join := r . pathJoin . ContainsPkgCallExpr ( call , c , false )
if join == nil {
return false
}
// We expect join.Args to include a baseDir-like arg and a cleaned path arg.
var foundBaseDir bool
var foundCleanArg bool
for _ , arg := range join . Args {
switch a := arg . ( type ) {
case * ast . BasicLit :
// literal string or similar — treat as possible baseDir
foundBaseDir = true
case * ast . Ident :
// If ident is resolvable to a constant string (TryResolve true), treat as baseDir.
// Or if ident refers to a variable that was itself assigned from a constant BasicLit,
// it's considered safe as baseDir.
if gosec . TryResolve ( a , c ) {
foundBaseDir = true
} else {
// It might be a cleaned variable: e.g. cleanPath := filepath.Clean(fn)
if r . isFilepathClean ( a , c ) {
foundCleanArg = true
}
}
case * ast . CallExpr :
// If an argument is a Clean() call directly, mark clean arg found.
if r . clean . ContainsPkgCallExpr ( a , c , false ) != nil {
foundCleanArg = true
}
default :
// ignore other types
}
}
return foundBaseDir && foundCleanArg
}
2023-02-15 20:44:13 +01:00
func ( r * readfile ) Match ( n ast . Node , c * gosec . Context ) ( * issue . Issue , error ) {
2025-09-11 23:47:46 +05:30
// Track filepath.Clean usages so identifiers assigned from Clean() are known.
2023-01-09 16:26:20 +01:00
if node := r . clean . ContainsPkgCallExpr ( n , c , false ) ; node != nil {
r . trackFilepathClean ( n )
return nil , nil
2025-09-11 23:47:46 +05:30
}
// Track Join assignments if we see an AssignStmt whose RHS is a Join call.
if assign , ok := n . ( * ast . AssignStmt ) ; ok {
// track join result assigned to a variable, e.g., fullPath := filepath.Join(baseDir, cleanPath)
r . trackJoinAssignStmt ( assign , c )
// also track Clean assignment if present on RHS
if len ( assign . Rhs ) > 0 {
if call , ok := assign . Rhs [ 0 ] . ( * ast . CallExpr ) ; ok {
if r . clean . ContainsPkgCallExpr ( call , c , false ) != nil {
r . trackFilepathClean ( call )
2018-08-27 21:34:07 -07:00
}
}
2025-09-11 23:47:46 +05:30
}
// continue, don't return here — other checks may apply
}
// Now check for file-reading calls (os.Open, os.OpenFile, ioutil.ReadFile etc.)
if node := r . ContainsPkgCallExpr ( n , c , false ) ; node != nil {
if len ( node . Args ) == 0 {
return nil , nil
}
arg := node . Args [ 0 ]
// If argument is a call expression, check for Join/Clean patterns.
if callExpr , ok := arg . ( * ast . CallExpr ) ; ok {
// If this call matches a safe Join(baseDir, Clean(...)) pattern, treat as safe.
if r . isSafeJoin ( callExpr , c ) {
// safe pattern detected; do not raise an issue
return nil , nil
}
// If the argument is a Join call but not safe per above, flag it (as before)
if r . isJoinFunc ( callExpr , c ) {
2025-09-16 02:12:05 +05:30
iss := c . NewIssue ( n , r . ID ( ) , r . What , r . Severity , r . Confidence )
if s := r . osRootSuggestion ( ) ; s != "" {
iss . Autofix = s
}
return iss , nil
2018-08-27 21:34:07 -07:00
}
2025-09-11 23:47:46 +05:30
}
2018-08-27 21:34:07 -07:00
2025-09-11 23:47:46 +05:30
// If arg is an identifier that was assigned from a Join(...) call, check that recorded Join call.
if ident , ok := arg . ( * ast . Ident ) ; ok {
if ident . Obj != nil {
if joinCall , ok := r . joinedVar [ ident . Obj . Decl ] ; ok {
// If the identifier itself was later cleaned, treat as safe regardless of original Join args
if r . isFilepathClean ( ident , c ) {
return nil , nil
}
// joinCall is a *ast.CallExpr; check if that join is a safe join
if jc , ok := joinCall . ( * ast . CallExpr ) ; ok {
if r . isSafeJoin ( jc , c ) {
return nil , nil
}
// join exists but is not safe: flag it
2025-09-16 02:12:05 +05:30
iss := c . NewIssue ( n , r . ID ( ) , r . What , r . Severity , r . Confidence )
if s := r . osRootSuggestion ( ) ; s != "" {
iss . Autofix = s
}
return iss , nil
2025-09-11 23:47:46 +05:30
}
2018-03-08 18:23:27 -05:00
}
}
}
2025-09-11 23:47:46 +05:30
// handles binary string concatenation eg. ioutil.Readfile("/tmp/" + file + "/blob")
if binExp , ok := arg . ( * ast . BinaryExpr ) ; ok {
// resolve all found identities from the BinaryExpr
if _ , ok := gosec . FindVarIdentities ( binExp , c ) ; ok {
2025-09-16 02:12:05 +05:30
iss := c . NewIssue ( n , r . ID ( ) , r . What , r . Severity , r . Confidence )
if s := r . osRootSuggestion ( ) ; s != "" {
iss . Autofix = s
}
return iss , nil
2025-09-11 23:47:46 +05:30
}
}
// if it's a plain identifier, and not resolved and not cleaned, flag it
if ident , ok := arg . ( * ast . Ident ) ; ok {
obj := c . Info . ObjectOf ( ident )
if _ , ok := obj . ( * types . Var ) ; ok &&
! gosec . TryResolve ( ident , c ) &&
! r . isFilepathClean ( ident , c ) {
2025-09-16 02:12:05 +05:30
iss := c . NewIssue ( n , r . ID ( ) , r . What , r . Severity , r . Confidence )
if s := r . osRootSuggestion ( ) ; s != "" {
iss . Autofix = s
}
return iss , nil
2025-09-11 23:47:46 +05:30
}
}
2018-03-08 18:23:27 -05:00
}
return nil , nil
}
// NewReadFile detects cases where we read files
2023-03-20 10:08:49 +01:00
func NewReadFile ( id string , _ gosec . Config ) ( gosec . Rule , [ ] ast . Node ) {
2018-03-09 11:27:41 +10:00
rule := & readfile {
2025-09-12 19:01:24 +05:30
pathJoin : gosec . NewCallList ( ) ,
clean : gosec . NewCallList ( ) ,
CallList : gosec . NewCallList ( ) ,
2023-02-15 20:44:13 +01:00
MetaData : issue . MetaData {
2018-03-09 11:27:41 +10:00
ID : id ,
What : "Potential file inclusion via variable" ,
2023-02-15 20:44:13 +01:00
Severity : issue . Medium ,
Confidence : issue . High ,
2018-03-09 12:49:01 +10:00
} ,
2023-01-09 16:26:20 +01:00
cleanedVar : map [ any ] ast . Node { } ,
2025-09-11 23:47:46 +05:30
joinedVar : map [ any ] ast . Node { } ,
2018-03-09 12:49:01 +10:00
}
2018-08-27 21:34:07 -07:00
rule . pathJoin . Add ( "path/filepath" , "Join" )
rule . pathJoin . Add ( "path" , "Join" )
2020-08-17 21:05:28 +02:00
rule . clean . Add ( "path/filepath" , "Clean" )
2020-08-19 08:39:55 +02:00
rule . clean . Add ( "path/filepath" , "Rel" )
2024-05-13 17:14:01 +02:00
rule . clean . Add ( "path/filepath" , "EvalSymlinks" )
2018-03-08 18:23:27 -05:00
rule . Add ( "io/ioutil" , "ReadFile" )
2021-10-14 15:53:26 +08:00
rule . Add ( "os" , "ReadFile" )
2018-03-08 18:23:27 -05:00
rule . Add ( "os" , "Open" )
2020-06-16 13:29:03 +02:00
rule . Add ( "os" , "OpenFile" )
2022-01-12 19:33:17 +01:00
rule . Add ( "os" , "Create" )
2025-09-11 23:47:46 +05:30
return rule , [ ] ast . Node { ( * ast . CallExpr ) ( nil ) , ( * ast . AssignStmt ) ( nil ) }
2018-03-08 18:23:27 -05:00
}