validateSingleExpression validates a single literal expression
(expression string, opts ExpressionValidationOptions)
| 178 | |
| 179 | // validateSingleExpression validates a single literal expression |
| 180 | func validateSingleExpression(expression string, opts ExpressionValidationOptions) error { |
| 181 | expression = strings.TrimSpace(expression) |
| 182 | |
| 183 | // Allow literal values (string, number, boolean) — safe leaf nodes in compound expressions. |
| 184 | if stringLiteralRegex.MatchString(expression) || |
| 185 | numberLiteralRegex.MatchString(expression) || |
| 186 | expression == "true" || expression == "false" { |
| 187 | return nil |
| 188 | } |
| 189 | |
| 190 | // Check for dangerous JavaScript property names (prototype pollution, PR #14826) |
| 191 | if err := validateExpressionForDangerousProps(expression); err != nil { |
| 192 | return err |
| 193 | } |
| 194 | |
| 195 | // Check if this expression is in the allowed list |
| 196 | allowed := false |
| 197 | |
| 198 | if opts.NeedsStepsRe.MatchString(expression) { |
| 199 | allowed = true |
| 200 | } else if opts.InputsRe.MatchString(expression) { |
| 201 | allowed = true |
| 202 | } else if opts.WorkflowCallInputsRe.MatchString(expression) { |
| 203 | allowed = true |
| 204 | } else if opts.AwInputsRe.MatchString(expression) { |
| 205 | allowed = true |
| 206 | } else if opts.AwImportInputsRe != nil && opts.AwImportInputsRe.MatchString(expression) { |
| 207 | allowed = true |
| 208 | } else if opts.EnvRe.MatchString(expression) { |
| 209 | allowed = true |
| 210 | } else if _, ok := constants.AllowedExpressionsSet[expression]; ok { |
| 211 | allowed = true |
| 212 | } |
| 213 | |
| 214 | // Check for OR expressions with literals (e.g., "inputs.repository || 'default'") |
| 215 | if !allowed { |
| 216 | orMatch := orExpressionPattern.FindStringSubmatch(expression) |
| 217 | if len(orMatch) > 2 { |
| 218 | leftExpr := strings.TrimSpace(orMatch[1]) |
| 219 | rightExpr := strings.TrimSpace(orMatch[2]) |
| 220 | |
| 221 | leftErr := validateSingleExpression(leftExpr, opts) |
| 222 | leftIsSafe := leftErr == nil && !containsExpressionInList(opts.UnauthorizedExpressions, leftExpr) |
| 223 | |
| 224 | if leftIsSafe { |
| 225 | // Check if right side is a literal string (single, double, or backtick quotes) |
| 226 | // Note: Using (?:) for non-capturing group and checking each quote type separately |
| 227 | isStringLiteral := stringLiteralRegex.MatchString(rightExpr) |
| 228 | // Check if right side is a number literal |
| 229 | isNumberLiteral := numberLiteralRegex.MatchString(rightExpr) |
| 230 | // Check if right side is a boolean literal |
| 231 | isBooleanLiteral := rightExpr == "true" || rightExpr == "false" |
| 232 | |
| 233 | if isStringLiteral || isNumberLiteral || isBooleanLiteral { |
| 234 | allowed = true |
| 235 | } else { |
| 236 | // If right side is also a safe expression, recursively check it |
| 237 | rightErr := validateSingleExpression(rightExpr, opts) |