mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-07 17:43:04 -10:00
feat(templates): require signatures for javascript templates (#7514)
* feat(templates): require signatures for javascript templates Unsigned javascript templates should not be accepted into execution paths when template signing is enforced. Apply the signature check to javascript protocol templates in both direct loading and workflows, and report those skips separately from code templates. Signed-off-by: Dwi Siswanto <[email protected]> * fix(signer): normalize line endings when signing The verifier already canonicalizes CRLF to LF before checking the digest, but the signer still hashed the raw file content. Templates signed from a CRLF checkout could fail verification later, which showed up on Windows after javascript fixtures started being signed in the integration harness. Use the same canonical content for signing and verification, and add a CRLF sign-and-verify regression. Signed-off-by: Dwi Siswanto <[email protected]> --------- Signed-off-by: Dwi Siswanto <[email protected]>
This commit is contained in:
1 parent
1427b35cdb
commit
fac0b7196d
15 files changed
+260
-41
No files matched your search
@@ -954,8 +954,10 @@ func (r *Runner) displayExecutionInfo(store *loader.Store) {
|
||||
r.Logger.Warning().Msg("No DAST templates found")
|
||||
}
|
||||
stats.ForceDisplayWarning(templates.SkippedUnverifiedCodeTemplateStats)
|
||||
stats.ForceDisplayWarning(templates.SkippedUnverifiedJavascriptTemplateStats)
|
||||
} else {
|
||||
stats.DisplayAsWarning(templates.SkippedUnverifiedCodeTemplateStats)
|
||||
stats.DisplayAsWarning(templates.SkippedUnverifiedJavascriptTemplateStats)
|
||||
}
|
||||
|
||||
stats.DisplayAsWarning(httpProtocol.SetThreadToCountZero)
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
)
|
||||
|
||||
var isCodeDisabled = func() bool { return osutils.IsWindows() && os.Getenv("CI") == "true" }
|
||||
var signedCodeTemplates sync.Once
|
||||
var signedCodeTemplatesErr error
|
||||
var signedIntegrationTemplates sync.Once
|
||||
var signedIntegrationTemplatesErr error
|
||||
|
||||
func hasAnyExecutable(names ...string) bool {
|
||||
for _, name := range names {
|
||||
@@ -51,23 +51,20 @@ const (
|
||||
|
||||
var testcertpath = ""
|
||||
|
||||
func ensureSignedCodeTemplates() error {
|
||||
if isCodeDisabled() {
|
||||
return nil
|
||||
}
|
||||
signedCodeTemplates.Do(func() {
|
||||
func ensureSignedIntegrationTemplates() error {
|
||||
signedIntegrationTemplates.Do(func() {
|
||||
previousWD, err := os.Getwd()
|
||||
if err != nil {
|
||||
signedCodeTemplatesErr = err
|
||||
signedIntegrationTemplatesErr = err
|
||||
return
|
||||
}
|
||||
if err := os.Chdir(suite.fixturesDir); err != nil {
|
||||
signedCodeTemplatesErr = err
|
||||
signedIntegrationTemplatesErr = err
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if chdirErr := os.Chdir(previousWD); chdirErr != nil && signedCodeTemplatesErr == nil {
|
||||
signedCodeTemplatesErr = chdirErr
|
||||
if chdirErr := os.Chdir(previousWD); chdirErr != nil && signedIntegrationTemplatesErr == nil {
|
||||
signedIntegrationTemplatesErr = chdirErr
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -79,13 +76,16 @@ func ensureSignedCodeTemplates() error {
|
||||
|
||||
tsigner, err := signer.NewTemplateSignerFromFiles(certPath, keyPath)
|
||||
if err != nil {
|
||||
signedCodeTemplatesErr = err
|
||||
signedIntegrationTemplatesErr = err
|
||||
return
|
||||
}
|
||||
|
||||
templatesToSign := []string{
|
||||
"workflow/code-template-1.yaml",
|
||||
"workflow/code-template-2.yaml",
|
||||
var templatesToSign []string
|
||||
if !isCodeDisabled() {
|
||||
templatesToSign = append(templatesToSign,
|
||||
"workflow/code-template-1.yaml",
|
||||
"workflow/code-template-2.yaml",
|
||||
)
|
||||
}
|
||||
for _, v := range codeTestCases {
|
||||
if v.DisableOn != nil && v.DisableOn() {
|
||||
@@ -102,14 +102,20 @@ func ensureSignedCodeTemplates() error {
|
||||
}
|
||||
templatesToSign = append(templatesToSign, v.Path)
|
||||
}
|
||||
for _, v := range jsTestcases {
|
||||
if v.DisableOn != nil && v.DisableOn() {
|
||||
continue
|
||||
}
|
||||
templatesToSign = append(templatesToSign, v.Path)
|
||||
}
|
||||
for _, templatePath := range templatesToSign {
|
||||
if err := templates.SignTemplate(tsigner, fixturePath(templatePath)); err != nil {
|
||||
signedCodeTemplatesErr = err
|
||||
signedIntegrationTemplatesErr = err
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
return signedCodeTemplatesErr
|
||||
return signedIntegrationTemplatesErr
|
||||
}
|
||||
|
||||
func getEnvValues() []string {
|
||||
@@ -118,6 +124,15 @@ func getEnvValues() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func runSignedNucleiTemplateAndGetResults(template, url string, debug bool, extra ...string) ([]string, error) {
|
||||
if err := ensureSignedIntegrationTemplates(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args := []string{"-t", template, "-target", url}
|
||||
args = append(args, extra...)
|
||||
return testutils.RunNucleiBareArgsAndGetResults(debug, getEnvValues(), args...)
|
||||
}
|
||||
|
||||
type codeSnippet struct{}
|
||||
|
||||
// Execute executes a test case and returns an error if occurred
|
||||
|
||||
@@ -19,8 +19,6 @@ import (
|
||||
"github.com/jcmturner/gokrb5/v8/iana/nametype"
|
||||
"github.com/jcmturner/gokrb5/v8/messages"
|
||||
"github.com/jcmturner/gokrb5/v8/types"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
|
||||
)
|
||||
|
||||
// javascriptASRepRoast exercises templates/ad/asrep-roast.yaml end-to-end
|
||||
@@ -46,7 +44,7 @@ func (j *javascriptASRepRoast) Execute(filePath string) error {
|
||||
}
|
||||
defer kdc.Close()
|
||||
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, kdc.Address(), debug)
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, kdc.Address(), debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ func newJavascriptDockerSpec(port string, options *dockertest.RunOptions, readyT
|
||||
type javascriptNetHttps struct{}
|
||||
|
||||
func (j *javascriptNetHttps) Execute(filePath string) error {
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "scanme.sh", debug)
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, "scanme.sh", debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -167,7 +167,7 @@ type javascriptMultiPortsSSH struct{}
|
||||
|
||||
func (j *javascriptMultiPortsSSH) Execute(filePath string) error {
|
||||
// use scanme.sh as target to ensure we match on the 2nd default port 22
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "scanme.sh", debug)
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, "scanme.sh", debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -177,7 +177,7 @@ func (j *javascriptMultiPortsSSH) Execute(filePath string) error {
|
||||
type javascriptNoPortArgs struct{}
|
||||
|
||||
func (j *javascriptNoPortArgs) Execute(filePath string) error {
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "yo.dawg", debug)
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, "yo.dawg", debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -192,7 +192,7 @@ func (j *javascriptWMICommand) Execute(filePath string) error {
|
||||
// the exclude list and short-circuits before any dial, so the JSON result
|
||||
// must contain "ok":false plus "network policy" in the error while not
|
||||
// leaking the password.
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug, "-eh", "203.0.113.10")
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug, "-eh", "203.0.113.10")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -204,7 +204,7 @@ type javascriptGoExecRedaction struct{}
|
||||
func (j *javascriptGoExecRedaction) Execute(filePath string) error {
|
||||
listener := newGoExecCloseListener()
|
||||
defer listener.Close()
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, listener.host, debug, "-V", "RPCEndpoint="+listener.binding)
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, listener.host, debug, "-V", "RPCEndpoint="+listener.binding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -216,7 +216,7 @@ type javascriptGoExecModules struct{}
|
||||
func (j *javascriptGoExecModules) Execute(filePath string) error {
|
||||
listener := newGoExecCloseListener()
|
||||
defer listener.Close()
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, listener.host, debug, "-V", "RPCEndpoint="+listener.binding)
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, listener.host, debug, "-V", "RPCEndpoint="+listener.binding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -275,7 +275,7 @@ func (j *javascriptGoExecSambaNTLM) Execute(filePath string) error {
|
||||
|
||||
errS := make([]error, 0, defaultRetry)
|
||||
for attempt := 1; attempt <= defaultRetry; attempt++ {
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug)
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug)
|
||||
if err == nil {
|
||||
if countErr := expectResultsCount(results, 1); countErr == nil {
|
||||
return nil
|
||||
@@ -383,7 +383,7 @@ func (j *networkMultiStep) Execute(filePath string) error {
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, server.URL, debug)
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, server.URL, debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -439,7 +439,7 @@ func runJavascriptDockerCase(filePath string, spec javascriptDockerSpec, expecte
|
||||
|
||||
errS := make([]error, 0, defaultRetry)
|
||||
for attempt := 1; attempt <= defaultRetry; attempt++ {
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, targetAddress, debug)
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, targetAddress, debug)
|
||||
if err == nil {
|
||||
if countErr := expectResultsCount(results, expectedNumbers...); countErr == nil {
|
||||
return nil
|
||||
|
||||
@@ -79,7 +79,7 @@ type javascriptNoAccess struct{}
|
||||
|
||||
// Execute executes a test case and returns an error if occurred
|
||||
func (h *javascriptNoAccess) Execute(filePath string) error {
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "trust_me_bro.real", debug, "-ms", "-j")
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, "trust_me_bro.real", debug, "-ms", "-j")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -159,9 +159,9 @@ func executeIntegrationCase(testCase integrationCase) error {
|
||||
if testCase.DisableOn != nil && testCase.DisableOn() {
|
||||
return nil
|
||||
}
|
||||
if needsSignedCodeTemplates(testCase.Path) {
|
||||
if err := ensureSignedCodeTemplates(); err != nil {
|
||||
return fmt.Errorf("failed to sign code templates: %w", err)
|
||||
if needsSignedIntegrationTemplates(testCase.Path) {
|
||||
if err := ensureSignedIntegrationTemplates(); err != nil {
|
||||
return fmt.Errorf("failed to sign integration templates: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,8 +191,10 @@ func parallelism() int {
|
||||
return parallelism
|
||||
}
|
||||
|
||||
func needsSignedCodeTemplates(path string) bool {
|
||||
return strings.HasPrefix(path, "protocols/code/") || strings.Contains(path, "workflow/code-")
|
||||
func needsSignedIntegrationTemplates(path string) bool {
|
||||
return strings.HasPrefix(path, "protocols/code/") ||
|
||||
strings.Contains(path, "workflow/code-") ||
|
||||
strings.HasPrefix(path, "protocols/javascript/")
|
||||
}
|
||||
|
||||
func testNameForPath(path string) string {
|
||||
|
||||
@@ -902,6 +902,16 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) ([]*temp
|
||||
return
|
||||
}
|
||||
|
||||
// javascript-protocol templates expose Go-backed modules through
|
||||
// the JS runtime, so unsigned ones are rejected before execution.
|
||||
if parsed.IsUnsignedJavascriptTemplate() {
|
||||
stats.Increment(templates.SkippedUnverifiedJavascriptTemplateStats)
|
||||
if config.DefaultConfig.LogAllEvents {
|
||||
store.logger.Warning().Msgf("Unverified javascript template at %q", templatePath)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if missingCaps := parsed.MissingLoadCapabilities(caps); len(missingCaps) > 0 {
|
||||
store.noteMissingCapabilities(templatePath, missingCaps)
|
||||
return
|
||||
|
||||
@@ -160,6 +160,122 @@ code:
|
||||
require.Equal(t, initialUnverified, stats.GetValue(templates.SkippedUnverifiedTemplateStats))
|
||||
}
|
||||
|
||||
func loadSingleTemplateForTest(t *testing.T, templatePath, executionID string) []*templates.Template {
|
||||
t.Helper()
|
||||
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.Logger = &gologger.Logger{}
|
||||
options.ExecutionId = executionID
|
||||
options.DisableUnsignedTemplates = false
|
||||
options.TemplateLoadingConcurrency = 1
|
||||
testutils.Init(options)
|
||||
t.Cleanup(func() {
|
||||
testutils.Cleanup(options)
|
||||
})
|
||||
|
||||
catalog := disk.NewCatalog("")
|
||||
executerOpts := testutils.NewMockExecuterOptions(options, nil)
|
||||
executerOpts.Catalog = catalog
|
||||
executerOpts.Parser = templates.NewParser()
|
||||
executerOpts.Logger = options.Logger
|
||||
|
||||
workflowLoader, err := workflow.NewLoader(executerOpts)
|
||||
require.NoError(t, err)
|
||||
executerOpts.WorkflowLoader = workflowLoader
|
||||
|
||||
store, err := New(NewConfig(options, catalog, executerOpts))
|
||||
require.NoError(t, err)
|
||||
|
||||
loaded, err := store.LoadTemplates([]string{templatePath})
|
||||
require.NoError(t, err)
|
||||
return loaded
|
||||
}
|
||||
|
||||
func TestLoadTemplatesRecordsUnsignedJavascriptTemplateOnlyAsJavascriptSkip(t *testing.T) {
|
||||
templatePath := filepath.Join(t.TempDir(), "unsigned-javascript.yaml")
|
||||
err := os.WriteFile(templatePath, []byte(`id: unsigned-javascript-template
|
||||
|
||||
info:
|
||||
name: Unsigned Javascript Template
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
javascript:
|
||||
- code: |
|
||||
Export("unsigned-javascript-template")
|
||||
`), 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
initialUnverifiedJavascript := stats.GetValue(templates.SkippedUnverifiedJavascriptTemplateStats)
|
||||
initialUnverified := stats.GetValue(templates.SkippedUnverifiedTemplateStats)
|
||||
|
||||
loaded := loadSingleTemplateForTest(t, templatePath, "loader-unsigned-javascript-template")
|
||||
require.Empty(t, loaded)
|
||||
require.Equal(t, initialUnverifiedJavascript+1, stats.GetValue(templates.SkippedUnverifiedJavascriptTemplateStats))
|
||||
require.Equal(t, initialUnverified, stats.GetValue(templates.SkippedUnverifiedTemplateStats))
|
||||
}
|
||||
|
||||
func TestLoadTemplatesTreatsMixedTemplateWithJavascriptAsJavascriptSensitive(t *testing.T) {
|
||||
templatePath := filepath.Join(t.TempDir(), "mixed-javascript.yaml")
|
||||
err := os.WriteFile(templatePath, []byte(`id: mixed-javascript-template
|
||||
|
||||
info:
|
||||
name: Mixed Javascript Template
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- mixed-javascript-template
|
||||
|
||||
javascript:
|
||||
- code: |
|
||||
Export("mixed-javascript-template")
|
||||
`), 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
initialUnverifiedJavascript := stats.GetValue(templates.SkippedUnverifiedJavascriptTemplateStats)
|
||||
|
||||
loaded := loadSingleTemplateForTest(t, templatePath, "loader-mixed-javascript-template")
|
||||
require.Empty(t, loaded)
|
||||
require.Equal(t, initialUnverifiedJavascript+1, stats.GetValue(templates.SkippedUnverifiedJavascriptTemplateStats))
|
||||
}
|
||||
|
||||
func TestLoadTemplatesAllowsUnsignedFlowTemplateWithoutJavascriptProtocol(t *testing.T) {
|
||||
templatePath := filepath.Join(t.TempDir(), "flow-only.yaml")
|
||||
err := os.WriteFile(templatePath, []byte(`id: unsigned-flow-template
|
||||
|
||||
info:
|
||||
name: Unsigned Flow Template
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
flow: http(1)
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- unsigned-flow-template
|
||||
`), 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
initialUnverifiedJavascript := stats.GetValue(templates.SkippedUnverifiedJavascriptTemplateStats)
|
||||
|
||||
loaded := loadSingleTemplateForTest(t, templatePath, "loader-unsigned-flow-template")
|
||||
require.Len(t, loaded, 1)
|
||||
require.Equal(t, "unsigned-flow-template", loaded[0].ID)
|
||||
require.Equal(t, initialUnverifiedJavascript, stats.GetValue(templates.SkippedUnverifiedJavascriptTemplateStats))
|
||||
}
|
||||
|
||||
func TestLoadTemplatesDoesNotRequireGlobalMatchersFlagToLoadTemplate(t *testing.T) {
|
||||
templatePath := filepath.Join(t.TempDir(), "global-matchers.yaml")
|
||||
err := os.WriteFile(templatePath, []byte(`id: global-matchers-template
|
||||
|
||||
@@ -284,6 +284,55 @@ workflows:
|
||||
require.Equal(t, initialUnverified, stats.GetValue(templates.SkippedUnverifiedTemplateStats))
|
||||
}
|
||||
|
||||
func Test_ParseWorkflowRecordsUnsignedJavascriptSubtemplateOnlyAsJavascriptSkip(t *testing.T) {
|
||||
setup()
|
||||
previousDisableUnsigned := executerOpts.Options.DisableUnsignedTemplates
|
||||
defer func() {
|
||||
executerOpts.Options.DisableUnsignedTemplates = previousDisableUnsigned
|
||||
}()
|
||||
|
||||
executerOpts.Options.DisableUnsignedTemplates = false
|
||||
|
||||
dir := t.TempDir()
|
||||
javascriptTemplatePath := filepath.Join(dir, "unsigned-javascript.yaml")
|
||||
err := os.WriteFile(javascriptTemplatePath, []byte(`id: workflow-unsigned-javascript
|
||||
|
||||
info:
|
||||
name: Workflow Unsigned Javascript
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
javascript:
|
||||
- code: |
|
||||
Export("workflow-unsigned-javascript")
|
||||
`), 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
workflowPath := filepath.Join(dir, "workflow.yaml")
|
||||
err = os.WriteFile(workflowPath, []byte(fmt.Sprintf(`id: workflow-unsigned-javascript-gate
|
||||
|
||||
info:
|
||||
name: Workflow Unsigned Javascript Gate
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
workflows:
|
||||
- template: %q
|
||||
`, javascriptTemplatePath)), 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
initialUnverifiedJavascript := stats.GetValue(templates.SkippedUnverifiedJavascriptTemplateStats)
|
||||
initialUnverified := stats.GetValue(templates.SkippedUnverifiedTemplateStats)
|
||||
|
||||
got, err := templates.Parse(workflowPath, nil, executerOpts)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got.CompiledWorkflow)
|
||||
require.Len(t, got.CompiledWorkflow.Workflows, 1)
|
||||
require.Empty(t, got.CompiledWorkflow.Workflows[0].Executers)
|
||||
require.Equal(t, initialUnverifiedJavascript+1, stats.GetValue(templates.SkippedUnverifiedJavascriptTemplateStats))
|
||||
require.Equal(t, initialUnverified, stats.GetValue(templates.SkippedUnverifiedTemplateStats))
|
||||
}
|
||||
|
||||
func Test_WrongTemplate(t *testing.T) {
|
||||
setup()
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ const (
|
||||
TemplateSyntaxErrorStats = "template-syntax-errors"
|
||||
TemplateSyntaxWarningStats = "template-syntax-warnings"
|
||||
|
||||
SkippedRequestSignatureTemplateStats = "skipped-request-signature-templates"
|
||||
SkippedUnverifiedCodeTemplateStats = "skipped-unverified-code-templates"
|
||||
SkippedUnverifiedTemplateStats = "skipped-unverified-templates"
|
||||
SkippedRequestSignatureTemplateStats = "skipped-request-signature-templates"
|
||||
SkippedUnverifiedCodeTemplateStats = "skipped-unverified-code-templates"
|
||||
SkippedUnverifiedJavascriptTemplateStats = "skipped-unverified-javascript-templates"
|
||||
SkippedUnverifiedTemplateStats = "skipped-unverified-templates"
|
||||
|
||||
ExcludedCodeTemplateStats = "excluded-code-templates"
|
||||
ExcludedDASTTemplateStats = "excluded-dast-templates"
|
||||
|
||||
@@ -73,6 +73,7 @@ func (t *TemplateSigner) GetUserFragment() string {
|
||||
// Sign signs the given template with the template signer and returns the signature
|
||||
func (t *TemplateSigner) Sign(data []byte, tmpl SignableTemplate) (string, error) {
|
||||
existingSignature, content := ExtractSignatureAndContent(data)
|
||||
content = normalizeTemplateContentForSignature(content)
|
||||
|
||||
// while re-signing template check if it has a code protocol
|
||||
// if it does then verify that it is signed by current signer
|
||||
@@ -146,9 +147,7 @@ func (t *TemplateSigner) Verify(data []byte, tmpl SignableTemplate) (bool, error
|
||||
return false, err
|
||||
}
|
||||
|
||||
// normalize content by removing \r\n everywhere since this only done for verification
|
||||
// it does not affect the actual template
|
||||
content = bytes.ReplaceAll(content, []byte("\r\n"), []byte("\n"))
|
||||
content = normalizeTemplateContentForSignature(content)
|
||||
|
||||
buff := bytes.NewBuffer(content)
|
||||
// if file has any imports process them
|
||||
@@ -164,6 +163,10 @@ func (t *TemplateSigner) Verify(data []byte, tmpl SignableTemplate) (bool, error
|
||||
return t.verify(buff.Bytes(), digest)
|
||||
}
|
||||
|
||||
func normalizeTemplateContentForSignature(content []byte) []byte {
|
||||
return bytes.ReplaceAll(content, []byte("\r\n"), []byte("\n"))
|
||||
}
|
||||
|
||||
// Verify verifies the given data with the template signer
|
||||
// Note: this should not be used for verifying templates as file references
|
||||
// in templates are not processed
|
||||
|
||||
@@ -48,6 +48,12 @@ func TestTemplateSignerSignAndVerify(t *testing.T) {
|
||||
tmpl: &mockSignableTemplate{},
|
||||
wantVerified: true,
|
||||
},
|
||||
{
|
||||
name: "Template with CRLF line endings",
|
||||
data: []byte("id: test-template\r\ninfo:\r\n name: Test Template"),
|
||||
tmpl: &mockSignableTemplate{},
|
||||
wantVerified: true,
|
||||
},
|
||||
{
|
||||
name: "Template with imports",
|
||||
data: []byte("id: test-template\ninfo:\n name: Test Template"),
|
||||
|
||||
@@ -24,6 +24,10 @@ var templateStatEntries = []templateStatEntry{
|
||||
name: SkippedUnverifiedCodeTemplateStats,
|
||||
description: "Found %d unsigned or tampered code template (carefully examine before using it & use -sign flag to sign them)",
|
||||
},
|
||||
{
|
||||
name: SkippedUnverifiedJavascriptTemplateStats,
|
||||
description: "Found %d unsigned or tampered javascript template (carefully examine before using it & use -sign flag to sign them)",
|
||||
},
|
||||
{
|
||||
name: ExcludedHeadlessTemplateStats,
|
||||
description: "Excluded %d headless template[s] (disabled as default), use -headless option to run headless templates.",
|
||||
|
||||
@@ -87,6 +87,12 @@ func (t *Template) HasJavascriptRequest(n ...int) bool {
|
||||
return HasRequest(t.RequestsJavascript, n...)
|
||||
}
|
||||
|
||||
// IsUnsignedJavascriptTemplate returns true if the template has a Javascript
|
||||
// protocol request but no verified template signature.
|
||||
func (t *Template) IsUnsignedJavascriptTemplate() bool {
|
||||
return t.HasJavascriptRequest() && !t.Verified
|
||||
}
|
||||
|
||||
// HasQueueRequests returns true if the template has queued requests.
|
||||
//
|
||||
// Queued requests contain all template requests in order (both protocol &
|
||||
|
||||
@@ -102,6 +102,13 @@ func parseWorkflowTemplate(workflow *workflows.WorkflowTemplate, preprocessor Pr
|
||||
continue
|
||||
}
|
||||
|
||||
if template.IsUnsignedJavascriptTemplate() {
|
||||
// unverified javascript templates are not allowed in workflows
|
||||
stats.Increment(SkippedUnverifiedJavascriptTemplateStats)
|
||||
gologger.Warning().Msgf("Skipping unverified javascript template(s) from workflow: %v\n", path)
|
||||
continue
|
||||
}
|
||||
|
||||
if missingCaps := template.MissingLoadCapabilities(caps); len(missingCaps) > 0 {
|
||||
for _, capability := range missingCaps {
|
||||
stats.Increment(capability.Stat())
|
||||
|
||||
Reference in new issue
Block a user