-
Notifications
You must be signed in to change notification settings - Fork 140
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add basic csp builder setup, most special-case handlers and tests * split logic into additional files, add tests * rename csp_builder to be same as package name, add docs in README * fix issues reported by golangci-lint
- Loading branch information
Showing
6 changed files
with
776 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
package cspbuilder | ||
|
||
import ( | ||
"strings" | ||
) | ||
|
||
const ( | ||
// Fetch Directives | ||
ChildSrc = "child-src" | ||
ConnectSrc = "connect-src" | ||
DefaultSrc = "default-src" | ||
FontSrc = "font-src" | ||
FrameSrc = "frame-src" | ||
ImgSrc = "img-src" | ||
ManifestSrc = "manifest-src" | ||
MediaSrc = "media-src" | ||
ObjectSrc = "object-src" | ||
PrefetchSrc = "prefetch-src" | ||
ScriptSrc = "script-src" | ||
ScriptSrcAttr = "script-src-attr" | ||
ScriptSrcElem = "script-src-elem" | ||
StyleSrc = "style-src" | ||
StyleSrcAttr = "style-src-attr" | ||
StyleSrcElem = "style-src-elem" | ||
WorkerSrc = "worker-src" | ||
|
||
// Document Directives | ||
BaseURI = "base-uri" | ||
Sandbox = "sandbox" | ||
|
||
// Navigation directives | ||
FormAction = "form-action" | ||
FrameAncestors = "frame-ancestors" | ||
NavigateTo = "navigate-to" | ||
|
||
// Reporting directives | ||
ReportURI = "report-uri" | ||
ReportTo = "report-to" | ||
|
||
// Other directives | ||
RequireTrustedTypesFor = "require-trusted-types-for" | ||
TrustedTypes = "trusted-types" | ||
UpgradeInsecureRequests = "upgrade-insecure-requests" | ||
) | ||
|
||
type Builder struct { | ||
Directives map[string]([]string) | ||
} | ||
|
||
// MustBuild is like Build but panics if an error occurs. | ||
func (builder *Builder) MustBuild() string { | ||
policy, err := builder.Build() | ||
if err != nil { | ||
panic(err) | ||
} | ||
return policy | ||
} | ||
|
||
// Build creates a content security policy string from the specified directives. | ||
// If any directive contains invalid values, an error is returned instead. | ||
func (builder *Builder) Build() (string, error) { | ||
var sb strings.Builder | ||
|
||
for directive := range builder.Directives { | ||
if sb.Len() > 0 { | ||
sb.WriteString("; ") | ||
} | ||
|
||
switch directive { | ||
case Sandbox: | ||
err := buildDirectiveSandbox(&sb, builder.Directives[directive]) | ||
if err != nil { | ||
return "", err | ||
} | ||
case FrameAncestors: | ||
err := buildDirectiveFrameAncestors(&sb, builder.Directives[directive]) | ||
if err != nil { | ||
return "", err | ||
} | ||
case ReportTo: | ||
err := buildDirectiveReportTo(&sb, builder.Directives[directive]) | ||
if err != nil { | ||
return "", err | ||
} | ||
case RequireTrustedTypesFor: | ||
err := buildDirectiveRequireTrustedTypesFor(&sb, builder.Directives[directive]) | ||
if err != nil { | ||
return "", err | ||
} | ||
case TrustedTypes: | ||
err := buildDirectiveTrustedTypes(&sb, builder.Directives[directive]) | ||
if err != nil { | ||
return "", err | ||
} | ||
case UpgradeInsecureRequests: | ||
err := buildDirectiveUpgradeInsecureRequests(&sb, builder.Directives[directive]) | ||
if err != nil { | ||
return "", err | ||
} | ||
default: | ||
// no special handling of directive values needed | ||
err := buildDirectiveDefault(&sb, directive, builder.Directives[directive]) | ||
if err != nil { | ||
return "", err | ||
} | ||
} | ||
} | ||
|
||
return sb.String(), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
package cspbuilder | ||
|
||
import ( | ||
"reflect" | ||
"sort" | ||
"strings" | ||
"testing" | ||
) | ||
|
||
func TestContentSecurityPolicyBuilder_Build_SingleDirective(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
directiveName string | ||
directiveValues []string | ||
want string | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "empty default-src", | ||
directiveName: DefaultSrc, | ||
directiveValues: nil, | ||
want: "", | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "single default-src", | ||
directiveName: DefaultSrc, | ||
directiveValues: []string{"'self'"}, | ||
want: "default-src 'self'", | ||
}, | ||
{ | ||
name: "multiple default-src", | ||
directiveName: DefaultSrc, | ||
directiveValues: []string{"'self'", "example.com", "*.example.com"}, | ||
want: "default-src 'self' example.com *.example.com", | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
builder := &Builder{ | ||
Directives: map[string][]string{ | ||
tt.directiveName: tt.directiveValues, | ||
}, | ||
} | ||
got, err := builder.Build() | ||
if (err != nil) != tt.wantErr { | ||
t.Errorf("ContentSecurityPolicyBuilder.Build() error = %v, wantErr %v", err, tt.wantErr) | ||
return | ||
} | ||
|
||
if got != tt.want { | ||
t.Errorf("ContentSecurityPolicyBuilder.Build() = '%v', want '%v'", got, tt.want) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestContentSecurityPolicyBuilder_Build_MultipleDirectives(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
directives map[string]([]string) | ||
builder Builder | ||
wantParts []string | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "multiple valid directives", | ||
directives: map[string]([]string){ | ||
"default-src": {"'self'", "example.com", "*.example.com"}, | ||
"sandbox": {"allow-scripts"}, | ||
"frame-ancestors": {"'self'", "http://*.example.com"}, | ||
"report-to": {"group1"}, | ||
"require-trusted-types-for": {"'script'"}, | ||
"trusted-types": {"policy-1", "policy-#=_/@.%", "'allow-duplicates'"}, | ||
"upgrade-insecure-requests": nil, | ||
}, | ||
|
||
wantParts: []string{ | ||
"default-src 'self' example.com *.example.com", | ||
"sandbox allow-scripts", | ||
"frame-ancestors 'self' http://*.example.com", | ||
"report-to group1", | ||
"require-trusted-types-for 'script'", | ||
"trusted-types policy-1 policy-#=_/@.% 'allow-duplicates'", | ||
"upgrade-insecure-requests", | ||
}, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
builder := &Builder{ | ||
Directives: tt.directives, | ||
} | ||
got, err := builder.Build() | ||
if (err != nil) != tt.wantErr { | ||
t.Errorf("ContentSecurityPolicyBuilder.Build() error = %v, wantErr %v", err, tt.wantErr) | ||
return | ||
} | ||
|
||
{ | ||
startsWithDirective := false | ||
for directive := range tt.directives { | ||
if strings.HasPrefix(got, directive) { | ||
startsWithDirective = true | ||
break | ||
} | ||
} | ||
if !startsWithDirective { | ||
t.Errorf("ContentSecurityPolicyBuilder.Build() = '%v', does not start with directive name", got) | ||
} | ||
} | ||
|
||
if strings.HasSuffix(got, " ") { | ||
t.Errorf("ContentSecurityPolicyBuilder.Build() = '%v', ends on whitespace", got) | ||
} | ||
if strings.HasSuffix(got, ";") { | ||
t.Errorf("ContentSecurityPolicyBuilder.Build() = '%v', ends on semi-colon", got) | ||
} | ||
|
||
// order of directives in created string is not guaranteed | ||
// check output contains all expected parts | ||
{ | ||
gotParts := strings.Split(got, "; ") | ||
if len(gotParts) != len(tt.wantParts) { | ||
t.Errorf("Got %d parts, want %d", len(gotParts), len(tt.wantParts)) | ||
} | ||
|
||
sort.Slice(gotParts, func(i, j int) bool { | ||
return gotParts[i] < gotParts[j] | ||
}) | ||
sort.Slice(tt.wantParts, func(i, j int) bool { | ||
return tt.wantParts[i] < tt.wantParts[j] | ||
}) | ||
|
||
if !reflect.DeepEqual(gotParts, tt.wantParts) { | ||
t.Errorf("ContentSecurityPolicyBuilder.Build() = '%v', expected following parts %v", got, gotParts) | ||
} | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.