Skip to content

Commit

Permalink
go-aah/aah#88 load and parse secure headers with safe defaults
Browse files Browse the repository at this point in the history
  • Loading branch information
jeevatkm committed Aug 9, 2017
1 parent af9e999 commit 2b285a3
Show file tree
Hide file tree
Showing 3 changed files with 287 additions and 3 deletions.
129 changes: 126 additions & 3 deletions security.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import (
"encoding/gob"
"errors"
"fmt"
"strings"
"sync"
"time"

"aahframework.org/ahttp.v0-unstable"
"aahframework.org/config.v0"
"aahframework.org/essentials.v0"
"aahframework.org/security.v0/authc"
Expand All @@ -34,9 +37,27 @@ type (
// Manager holds aah security management and its implementation.
Manager struct {
SessionManager *session.Manager
SecureHeaders *SecureHeaders
IsSSLEnabled bool
appCfg *config.Config
authSchemes map[string]scheme.Schemer
}

// SecureHeaders holds the composed values of HTTP security headers
// based on config `security.http_header.*` from `security.conf`.
SecureHeaders struct {
Common map[string]string

// Applied to all HTTPS response.
STS string
PKP string
PKPReportOnly bool

// Applied to all HTML Content-Type
XSSFilter string
CSP string
CSPReportOnly bool
}
)

// New method creates the security manager initial values and returns it.
Expand All @@ -53,8 +74,12 @@ func New() *Manager {
// Init method initialize the application security configuration `security { ... }`.
// Which is mainly Session, CORS, CSRF, Security Headers, etc.
func (m *Manager) Init(appCfg *config.Config) error {
var err error
m.appCfg = appCfg

// Initialize Secure Headers
m.initializeSecureHeaders()

// Initialize Auth Schemes
keyPrefixAuthScheme := "security.auth_schemes"
for _, keyAuthScheme := range m.appCfg.KeysByPath(keyPrefixAuthScheme) {
Expand All @@ -73,17 +98,20 @@ func (m *Manager) Init(appCfg *config.Config) error {
}

// Initialize the auth scheme
if err := authScheme.Init(m.appCfg, keyAuthScheme); err != nil {
if err = authScheme.Init(m.appCfg, keyAuthScheme); err != nil {
return err
}
}

// Initialize session manager
var err error
if m.SessionManager, err = session.NewManager(m.appCfg); err != nil {
return err
}
_ = m

// Based on aah server SSL configuration `http.Cookie.Secure` value is set, even
// though it's true in the aah.conf at `security.session.secure = true`.
m.SessionManager.Options.Secure = m.IsSSLEnabled

return nil
}

Expand Down Expand Up @@ -116,6 +144,93 @@ func (m *Manager) IsAuthSchemesConfigured() bool {
return len(m.authSchemes) != 0
}

//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Manager Unexported methods
//___________________________________

func (m *Manager) initializeSecureHeaders() {
keyPrefix := "security.http_header."
cfg := m.appCfg

m.SecureHeaders = new(SecureHeaders)

// Common
common := make(map[string]string)

// Header: X-Frame-Options
if xfo := cfg.StringDefault(keyPrefix+"xfo", "SAMEORIGIN"); !ess.IsStrEmpty(xfo) {
common[ahttp.HeaderXFrameOptions] = strings.TrimSpace(xfo)
}

// Header: X-Content-Type-Options
if xcto := cfg.StringDefault(keyPrefix+"xcto", "nosniff"); !ess.IsStrEmpty(xcto) {
common[ahttp.HeaderXContentTypeOptions] = strings.TrimSpace(xcto)
}

// Header: Referrer-Policy
if rp := cfg.StringDefault(keyPrefix+"rp", "no-referrer-when-downgrade"); !ess.IsStrEmpty(rp) {
common[ahttp.HeaderReferrerPolicy] = strings.TrimSpace(rp)
}

// Header: X-Permitted-Cross-Domain-Policies
if xpcdp := cfg.StringDefault(keyPrefix+"xpcdp", "master-only"); !ess.IsStrEmpty(xpcdp) {
common[ahttp.HeaderXPermittedCrossDomainPolicies] = strings.TrimSpace(xpcdp)
}

// Set common headers
m.SecureHeaders.Common = common

// Header: X-XSS-Protection, applied to all HTML Content-Type
m.SecureHeaders.XSSFilter = strings.TrimSpace(cfg.StringDefault(keyPrefix+"xxssp", "1; mode=block"))

// Header: Strict-Transport-Security, applied to all HTTPS response.
sts := "max-age=" + parseToSecondsString(
cfg.StringDefault(keyPrefix+"sts.max_age", "720h"),
2592000) // 30 days
if cfg.BoolDefault(keyPrefix+"sts.include_subdomains", false) {
sts += "; includeSubDomains"
}
if cfg.BoolDefault(keyPrefix+"sts.preload", false) {
sts += "; preload"
}
m.SecureHeaders.STS = strings.TrimSpace(sts)

// Header: Content-Security-Policy, to all HTML Content-Type
if csp := cfg.StringDefault(keyPrefix+"csp.directives", ""); !ess.IsStrEmpty(csp) {
// Add Report URI
if reportURI := cfg.StringDefault(keyPrefix+"csp.report_uri", "false"); !ess.IsStrEmpty(reportURI) {
csp += "; report-uri " + strings.TrimSpace(reportURI)
}
m.SecureHeaders.CSP = strings.TrimSpace(csp)
m.SecureHeaders.CSPReportOnly = cfg.BoolDefault(keyPrefix+"csp.report_only", false)
}

// Header: Public-Key-Pins, applied to all HTTPS response.
if pkpKeys, found := cfg.StringList(keyPrefix + "pkp.keys"); found && len(pkpKeys) > 0 {
pkp := []string{}
for _, key := range pkpKeys {
pkp = append(pkp, ` pin-sha256="`+key+`"`)
}

// Max Age
pkp = append(pkp, " max-age="+parseToSecondsString(
cfg.StringDefault(keyPrefix+"pkp.max_age", "720h"), 2592000))

// Include Subdomains
if cfg.BoolDefault(keyPrefix+"pkp.include_subdomains", false) {
pkp = append(pkp, " includeSubdomains")
}

// Add Report URI
if reportURI := cfg.StringDefault(keyPrefix+"pkp.report_uri", ""); !ess.IsStrEmpty(reportURI) {
pkp = append(pkp, " report-uri="+reportURI)
}

m.SecureHeaders.PKP = strings.TrimSpace(strings.Join(pkp, ";"))
m.SecureHeaders.PKPReportOnly = cfg.BoolDefault(keyPrefix+"pkp.report_only", false)
}
}

//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Package methods
//___________________________________
Expand All @@ -142,6 +257,14 @@ func ReleaseSubject(s *Subject) {
// Unexported methods
//___________________________________

func parseToSecondsString(durationStr string, defaultOnErr int64) string {
value, err := time.ParseDuration(durationStr)
if err != nil {
value = time.Second * time.Duration(defaultOnErr)
}
return fmt.Sprintf("%v", int64(value.Seconds()))
}

func init() {
gob.Register(&authc.AuthenticationInfo{})
gob.Register(&authc.Principal{})
Expand Down
12 changes: 12 additions & 0 deletions security_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ func TestSecurityInit(t *testing.T) {

authScheme = sec.GetAuthScheme("no_auth")
assert.Nil(t, authScheme)

// Validate Secure headers
assert.Equal(t, "SAMEORIGIN", sec.SecureHeaders.Common["X-Frame-Options"])
assert.Equal(t, "nosniff", sec.SecureHeaders.Common["X-Content-Type-Options"])
assert.Equal(t, "no-referrer-when-downgrade", sec.SecureHeaders.Common["Referrer-Policy"])
assert.Equal(t, "master-only", sec.SecureHeaders.Common["X-Permitted-Cross-Domain-Policies"])
assert.Equal(t, "max-age=31536000; includeSubDomains; preload", sec.SecureHeaders.STS)
assert.Equal(t, `pin-sha256="X3pGTSOuJeEVw989IJ/cEtXUEmy52zs1TZQrU06KUKg="; pin-sha256="MHJYVThihUrJcxW6wcqyOISTXIsInsdj3xK8QrZbHec="; pin-sha256="GGekerhihUrJcxW6wcqyOISTXIsInsdj3xK8QrZbHec="; max-age=2592000; includeSubdomains; report-uri=http://report.localhost`, sec.SecureHeaders.PKP)
assert.Equal(t, "1; mode=block", sec.SecureHeaders.XSSFilter)
assert.Equal(t, "default-src 'none'; report-uri http://report.localhost", sec.SecureHeaders.CSP)
assert.True(t, sec.SecureHeaders.CSPReportOnly)
assert.True(t, sec.SecureHeaders.PKPReportOnly)
}

func TestSecurityInitError(t *testing.T) {
Expand Down
149 changes: 149 additions & 0 deletions testdata/security.conf
Original file line number Diff line number Diff line change
@@ -1,6 +1,155 @@
# test security config

security {
# --------------------------------------------------------------------
# HTTP Secure Header(s)
# Application of security headers with many safe defaults.
# Typically non-empty config values included with appropriate
# HTTP header in the response.
#
# Note: Secure Headers highly important to web application.
#
# Tip: Quick way to verify secure headers - https://securityheaders.io
# --------------------------------------------------------------------
http_header {
# X-XSS-Protection
# Designed to enable the cross-site scripting (XSS) filter built into modern
# web browsers. This is usually enabled by default, but using it will enforce it.
#
# Learn more:
# https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#xxxsp
# https://www.keycdn.com/blog/x-xss-protection/
#
# Encouraged to make use of header `Content-Security-Policy` with enhanced
# policy to reduce XSS risk along with `X-XSS-Protection`.
xxssp = "1; mode=block"

# X-Content-Type-Options (XCTO)
# Prevent Content Sniffing or MIME sniffing.
#
# Learn more:
# https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#xcto
# https://en.wikipedia.org/wiki/Content_sniffing
xcto = "nosniff"

# X-Frame-Options (XFO)
# Prevent Clickjacking
#
# Learn more:
# https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#xfo
# https://www.keycdn.com/blog/x-frame-options/
xfo = "SAMEORIGIN"

# Referrer-Policy
# This header governs which referrer information, sent in the Referer header, should
# be included with requests made.
# Referrer Policy has been a W3C Candidate Recommendation since 26 January 2017.
#
# Learn more:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
# https://scotthelme.co.uk/a-new-security-header-referrer-policy/
# https://www.w3.org/TR/referrer-policy/
rp = "no-referrer-when-downgrade"

# Strict-Transport-Security (STS, aka HSTS)
# Security feature that lets a web site tell browsers that it should only be communicated
# with using HTTPS, instead of using HTTP.
#
# Learn more:
# https://www.owasp.org/index.php/HTTP_Strict_Transport_Security_Cheat_Sheet
# https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
#
# Note: Only applied to HTTPS(SSL) connection
sts {
# The time, in seconds, that the browser should remember that this site
# is only to be accessed using HTTPS. Valid time units are
# "s -> seconds", "m -> minutes", "h - hours".
# Default value is 1 year in hours.
max_age = "8760h"

# If enabled the STS rule applies to all of the site's subdomains as well.
# Default value is `false`.
include_subdomains = true

# Before enabling preload option, please read about pros and cons from above links.
# Default value is `false`.
preload = true
}

# Content-Security-Policy (CSP)
# Provides a rich set of policy directives that enable fairly granular control
# over the resources that a page is allowed. Prevents XSS risks.
#
# Learn more:
# https://content-security-policy.com/ (In-depth information and samples)
# https://developers.google.com/web/fundamentals/security/csp/
# https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#csp
#
# Read above references and define your policy.
csp {
# Set of directives to govern the resources load on a page.
directives = "default-src 'none'"

# By default, violation reports aren't sent. To enable violation reporting,
# you need to specify the report-uri policy directive.
report_uri = "http://report.localhost"

# Puts your `Content-Security-Policy` in report only mode, so that you can verify
# and then set `csp_report_only` value to false.
# Don't forget to set the `report-uri` for validation.
report_only = true
}

# Public-Key-Pins PKP (aka HPKP)
# This header prevents the Man-in-the-Middle Attack (MITM) with forged certificates.
#
# Learn more:
# https://scotthelme.co.uk/hpkp-http-public-key-pinning/
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning
# Read above references and define your keys.
#
# Note:
# - HPKP has the potential to lock out users for a long time if used incorrectly!
# The use of backup certificates and/or pinning the CA certificate is recommended.
# - Only applied to HTTPS(SSL) connection
pkp {
# The Base64 encoded Subject Public Key Information (SPKI) fingerprint.
# These values gets added as `pin-sha256=<key1>; ...`.
keys = [
"X3pGTSOuJeEVw989IJ/cEtXUEmy52zs1TZQrU06KUKg=",
"MHJYVThihUrJcxW6wcqyOISTXIsInsdj3xK8QrZbHec=",
"GGekerhihUrJcxW6wcqyOISTXIsInsdj3xK8QrZbHec=",
]

# The time that the browser should remember that this site is only to be
# accessed using one of the defined keys.
# Valid time units are "s -> seconds", "m -> minutes", "h - hours".
max_age = "720h"

# If enabled the PKP keys applies to all of the site's subdomains as well.
# Default value is `false`.
include_subdomains = true

# By default, Pin validation failure reports aren't sent. To enable Pin validation
# failure reporting, you need to specify the report-uri.
report_uri = "http://report.localhost"

# Puts your `Public-Key-Pins` in report only mode, so that you can verify
# and then set `pkp_report_only` value to false.
# Don't forget to set the `report-uri` for validation.
report_only = true
}

# X-Permitted-Cross-Domain-Policies
# Restrict Adobe Flash Player's or PDF documents access via crossdomain.xml,
# and this header.
#
# Learn more:
# https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#xpcdp
# https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html
xpcdp = "master-only"
}

auth_schemes {
# HTTP Form Auth Scheme
form_auth {
Expand Down

0 comments on commit 2b285a3

Please sign in to comment.