diff --git a/.github/workflows/web-scan.yml b/.github/workflows/web-scan.yml new file mode 100644 index 00000000..e179decf --- /dev/null +++ b/.github/workflows/web-scan.yml @@ -0,0 +1,66 @@ +name: 'Security web scan for zot' +on: + push: + branches: + - main + pull_request: + branches: + - main + release: + types: + - published + +permissions: + contents: read + +jobs: + zap_scan: + runs-on: ubuntu-latest + name: Scan ZOT using ZAP + strategy: + matrix: + flavor: [zot-linux-amd64-minimal, zot-linux-amd64] + steps: + - name: Install go + uses: actions/setup-go@v3 + with: + go-version: 1.19.x + - name: Checkout + uses: actions/checkout@v3 + - name: Build zot + run: | + echo "Building $FLAVOR" + cd $GITHUB_WORKSPACE + if [[ $FLAVOR == "zot-linux-amd64-minimal" ]]; then + make binary-minimal + else + make binary + fi + ls -l bin/ + env: + FLAVOR: ${{ matrix.flavor }} + - name: Bringup zot server + run: | + # upload images, zot can serve OCI image layouts directly like so + mkdir /tmp/zot + skopeo copy --format=oci docker://busybox:latest oci:/tmp/zot/busybox:latest + # start zot + if [[ $FLAVOR == "zot-linux-amd64-minimal" ]]; then + ./bin/${{ matrix.flavor }} serve examples/config-conformance.json & + else + ./bin/${{ matrix.flavor }} serve examples/config-ui.json & + fi + # wait until service is up + while true; do x=0; curl -f http://localhost:8080/v2/ || x=1; if [ $x -eq 0 ]; then break; fi; sleep 1; done + env: + FLAVOR: ${{ matrix.flavor }} + - name: ZAP Scan Rest API + uses: zaproxy/action-baseline@v0.7.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + docker_name: 'owasp/zap2docker-stable' + target: 'http://localhost:8080/v2/' + rules_file_name: '.zap/rules.tsv' + cmd_options: '-a -j' + allow_issue_writing: false + fail_action: true diff --git a/.zap/rules.tsv b/.zap/rules.tsv new file mode 100644 index 00000000..1d423e68 --- /dev/null +++ b/.zap/rules.tsv @@ -0,0 +1,63 @@ +# zap-baseline rule configuration file +# Change WARN to IGNORE to ignore rule or FAIL to fail if rule matches +# Only the rule identifiers are used - the names are just for info +# You can add your own messages to each rule by appending them after a tab on each line. +10003 WARN (Vulnerable JS Library (Powered by Retire.js)) +10009 WARN (In Page Banner Information Leak) +10010 WARN (Cookie No HttpOnly Flag) +10011 WARN (Cookie Without Secure Flag) +10015 WARN (Re-examine Cache-control Directives) +10017 WARN (Cross-Domain JavaScript Source File Inclusion) +10019 WARN (Content-Type Header Missing) +10020 WARN (Anti-clickjacking Header) +10021 WARN (X-Content-Type-Options Header Missing) +10023 WARN (Information Disclosure - Debug Error Messages) +10024 WARN (Information Disclosure - Sensitive Information in URL) +10025 WARN (Information Disclosure - Sensitive Information in HTTP Referrer Header) +10026 WARN (HTTP Parameter Override) +10027 IGNORE (Information Disclosure - Suspicious Comments) The comments have been reviewed and will not help an attacker +10028 WARN (Open Redirect) +10029 WARN (Cookie Poisoning) +10030 WARN (User Controllable Charset) +10031 WARN (User Controllable HTML Element Attribute (Potential XSS)) +10032 WARN (Viewstate) +10033 WARN (Directory Browsing) +10034 WARN (Heartbleed OpenSSL Vulnerability (Indicative)) +10035 WARN (Strict-Transport-Security Header) +10036 WARN (HTTP Server Response Header) +10037 WARN (Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s)) +10038 WARN (Content Security Policy (CSP) Header Not Set) +10039 WARN (X-Backend-Server Header Information Leak) +10040 WARN (Secure Pages Include Mixed Content) +10041 WARN (HTTP to HTTPS Insecure Transition in Form Post) +10042 WARN (HTTPS to HTTP Insecure Transition in Form Post) +10043 WARN (User Controllable JavaScript Event (XSS)) +10044 WARN (Big Redirect Detected (Potential Sensitive Information Leak)) +10049 IGNORE (Content Cacheability) We'd need to set the non-cacheble headers on content which could potentially be cached +10050 WARN (Retrieved from Cache) +10052 WARN (X-ChromeLogger-Data (XCOLD) Header Information Leak) +10054 WARN (Cookie without SameSite Attribute) +10055 WARN (CSP) +10056 WARN (X-Debug-Token Information Leak) +10057 WARN (Username Hash Found) +10061 WARN (X-AspNet-Version Response Header) +10062 WARN (PII Disclosure) +10063 WARN (Permissions Policy Header Not Set) +10096 IGNORE (Timestamp Disclosure) All existing timestamps are related to container images and are required +10097 WARN (Hash Disclosure) +10098 IGNORE (Cross-Domain Misconfiguration) Cannot know in advance what DN the users will configure for CORS headers +10105 IGNORE (Weak Authentication Method) Cannot package in advance a certificate which would be used for the user's domain, so we cannot use HTTPS +10108 WARN (Reverse Tabnabbing) +10109 IGNORE (Modern Web Application) The Ajax crawler is run using -j command line option +10110 WARN (Dangerous JS Functions) +10202 WARN (Absence of Anti-CSRF Tokens) +2 WARN (Private IP Disclosure) +3 WARN (Session ID in URL Rewrite) +50001 WARN (Script Passive Scan Rules) +90001 WARN (Insecure JSF ViewState) +90002 WARN (Java Serialization Object) +90003 IGNORE (Sub Resource Integrity Attribute Missing) Google Fonts API return dynamic stylesheets depending on OS/Browser and it is not possible to use static identity hashes +90011 WARN (Charset Mismatch) +90022 WARN (Application Error Disclosure) +90030 WARN (WSDL File Detection) +90033 WARN (Loosely Scoped Cookie) diff --git a/Makefile b/Makefile index e1293a9b..896d7e83 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ REGCLIENT := $(TOOLSDIR)/bin/regctl REGCLIENT_VERSION := v0.4.5 ACTION_VALIDATOR := $(TOOLSDIR)/bin/action-validator ACTION_VALIDATOR_VERSION := v0.2.1 -ZUI_VERSION := v2.0.0-rc3 +ZUI_VERSION := commit-1cf9b3c STACKER := $(TOOLSDIR)/bin/stacker BATS := $(TOOLSDIR)/bin/bats TESTDATA := $(TOP_LEVEL)/test/data diff --git a/golangcilint.yaml b/golangcilint.yaml index bce11f28..bae8da48 100644 --- a/golangcilint.yaml +++ b/golangcilint.yaml @@ -32,6 +32,8 @@ linters-settings: - w *os.File - to int64 - l *ldap.Conn + - w http.ResponseWriter + - r *http.Request gci: sections: - standard diff --git a/pkg/extensions/extension_search.go b/pkg/extensions/extension_search.go index a18b4790..9de3f16d 100644 --- a/pkg/extensions/extension_search.go +++ b/pkg/extensions/extension_search.go @@ -4,6 +4,7 @@ package extensions import ( + "net/http" "time" gqlHandler "github.com/99designs/gqlgen/graphql/handler" @@ -76,6 +77,14 @@ func downloadTrivyDB(cveInfo CveInfo, log log.Logger, updateInterval time.Durati } } +func addSearchSecurityHeaders(h http.Handler) http.HandlerFunc { //nolint:varnamelen + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Content-Type-Options", "nosniff") + + h.ServeHTTP(w, r) + } +} + func SetupSearchRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController, repoDB repodb.RepoDB, cveInfo CveInfo, log log.Logger, ) { @@ -86,7 +95,7 @@ func SetupSearchRoutes(config *config.Config, router *mux.Router, storeControlle extRouter := router.PathPrefix(constants.ExtSearchPrefix).Subrouter() extRouter.Methods("GET", "POST", "OPTIONS"). - Handler(gqlHandler.NewDefaultServer(gql_generated.NewExecutableSchema(resConfig))) + Handler(addSearchSecurityHeaders(gqlHandler.NewDefaultServer(gql_generated.NewExecutableSchema(resConfig)))) } } diff --git a/pkg/extensions/extension_ui.go b/pkg/extensions/extension_ui.go index 361b5b3a..54cf4b00 100644 --- a/pkg/extensions/extension_ui.go +++ b/pkg/extensions/extension_ui.go @@ -33,6 +33,17 @@ func (uih uiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +func addUISecurityHeaders(h http.Handler) http.HandlerFunc { //nolint:varnamelen + return func(w http.ResponseWriter, r *http.Request) { + permissionsPolicy := "microphone=(), geolocation=(), battery=(), camera=(), autoplay=(), gyroscope=(), payment=()" + w.Header().Set("Permissions-Policy", permissionsPolicy) + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + + h.ServeHTTP(w, r) + } +} + func SetupUIRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController, log log.Logger, ) { @@ -40,11 +51,11 @@ func SetupUIRoutes(config *config.Config, router *mux.Router, storeController st fsub, _ := fs.Sub(content, "build") uih := uiHandler{log: log} - router.PathPrefix("/login").Handler(uih) - router.PathPrefix("/home").Handler(uih) - router.PathPrefix("/explore").Handler(uih) - router.PathPrefix("/image").Handler(uih) - router.PathPrefix("/").Handler(http.FileServer(http.FS(fsub))) + router.PathPrefix("/login").Handler(addUISecurityHeaders(uih)) + router.PathPrefix("/home").Handler(addUISecurityHeaders(uih)) + router.PathPrefix("/explore").Handler(addUISecurityHeaders(uih)) + router.PathPrefix("/image").Handler(addUISecurityHeaders(uih)) + router.PathPrefix("/").Handler(addUISecurityHeaders(http.FileServer(http.FS(fsub)))) log.Info().Msg("setting up ui routes") }