var attributes cronAttributes if err = mapstructure.Decode(cronTrigger.Attributes, &attributes); err != nil { return nil, errors.Wrap(err, "Failed to decode cron trigger attributes") } // populate schedule if attributes.Interval != "" { spec.Schedule = fmt.Sprintf("@every %s", attributes.Interval) } else { spec.Schedule, err = lc.normalizeCronTriggerScheduleInput(attributes.Schedule) if err != nil { return nil, errors.Wrap(err, "Failed to normalize cron schedule") } } functionAddress, err := lc.getCronTriggerInvocationURL(resources, function.Namespace) if err != nil { return nil, errors.Wrap(err, "Failed to get cron trigger invocation URL") } // Build curl args using exec form so the CronJob container invokes curl directly, // without a shell. This prevents user-supplied header keys/values or event body // from being interpreted as shell syntax (see GHSA-v5px-423j-pf7p). curlArgs := []string{"--silent"} // user-supplied headers, sorted for deterministic ordering across reconciles userHeaderKeys := make([]string, 0, len(attributes.Event.Headers)) for headerKey := range attributes.Event.Headers { userHeaderKeys = append(userHeaderKeys, headerKey) } sort.Strings(userHeaderKeys) for _, headerKey := range userHeaderKeys { curlArgs = append(curlArgs, "--header", fmt.Sprintf("%s: %s", headerKey, attributes.Event.GetHeaderString(headerKey))) } // default headers curlArgs = append(curlArgs, "--header", fmt.Sprintf("%s: %s", headers.InvokeTrigger, "cron"), "--header", fmt.Sprintf("%s: %s", headers.TargetName, function.Name), ) // event body, compacted as JSON when valid (for size/readability, not for safety) if attributes.Event.Body != "" { eventBody := attributes.Event.Body eventBodyAsCompactedJSON := bytes.NewBuffer([]byte{}) if err := json.Compact(eventBodyAsCompactedJSON, []byte(eventBody)); err == nil { eventBody = eventBodyAsCompactedJSON.String() } // use --data-raw, not --data: --data treats a leading '@' as "load file" // (and '@-' as "read stdin"), which would let a function spec author exfiltrate // a file from the CronJob pod via a body of e.g. "@/etc/passwd". curlArgs = append(curlArgs, "--data-raw", eventBody) } // retry settings and target URL (kept last so curl sees them after all flags) curlArgs = append(curlArgs, "--retry", "10", "--retry-delay", "1", "--retry-max-time", "10", "--retry-connrefused", functionAddress, ) // get cron job retries until failing a job (default=2) jobBackoffLimit := attributes.JobBackoffLimit if jobBackoffLimit == 0 { jobBackoffLimit = 2 } spec.JobTemplate = batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ BackoffLimit: &jobBackoffLimit, Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "function-invocator", Image: common.GetEnvOrDefaultString( "NUCLIO_CONTROLLER_CRON_TRIGGER_CRON_JOB_IMAGE_NAME", "gcr.io/iguazio/curlimages/curl:7.81.0"), Command: []string{"curl"}, Args: curlArgs, ImagePullPolicy: v1.PullPolicy(common.GetEnvOrDefaultString("NUCLIO_CONTROLLER_CRON_TRIGGER_CRON_JOB_IMAGE_PULL_POLICY", "IfNotPresent")), }, }, RestartPolicy: v1.RestartPolicyNever, NodeSelector: function.Status.EnrichedNodeSelector, NodeName: function.Spec.NodeName, Affinity: function.Spec.Affinity, PriorityClassName: function.Spec.PriorityClassName, }, }, }, }