Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pscanrulesBeta: handle credentialless COEP directive #6180

Merged
merged 13 commits into from
Feb 19, 2025

Conversation

Dxk09
Copy link
Contributor

@Dxk09 Dxk09 commented Feb 11, 2025

Overview

Add support for 'credentialless' COEP value in Site Isolation scan rule.

This is the new PR for #6175

Related Issues

Checklist

  • Update help
  • Update changelog
  • Run ./gradlew spotlessApply for code formatting
  • Write tests
  • Check code coverage
  • Sign-off commits
  • Squash commits
  • Use a descriptive title

@kingthorin
Copy link
Member

I'll provide a full copy of the unit test class in the morning.

@psiinon
Copy link
Member

psiinon commented Feb 11, 2025

Logo
Checkmarx One – Scan Summary & Details690d0fbe-e74b-4333-9888-fa1c9b256460

Great job, no security vulnerabilities found in this Pull Request

@kingthorin
Copy link
Member

Here's what the full test class should look like:

SiteIsolationScanRuleTest.java (Click the triangle/control to the left to expand)
/*
 * Zed Attack Proxy (ZAP) and its related class files.
 *
 * ZAP is an HTTP/HTTPS proxy for assessing web application security.
 *
 * Copyright 2020 The ZAP Development Team
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.zaproxy.zap.extension.pscanrulesBeta;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;

import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.parosproxy.paros.core.scanner.Alert;
import org.parosproxy.paros.network.HttpMessage;
import org.zaproxy.addon.commonlib.CommonAlertTag;

class SiteIsolationScanRuleTest extends PassiveScannerTest<SiteIsolationScanRule> {
    @Test
    void shouldRaiseCorpAlertGivenSiteIsNotIsolated() throws Exception {
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader("HTTP/1.1 200 OK\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(1));
        assertThat(
                alertsRaised.get(0).getParam(),
                equalTo(SiteIsolationScanRule.CorpHeaderScanRule.HEADER));
        assertThat(alertsRaised.get(0).getEvidence(), equalTo(""));
    }

    @Test
    void shouldNotRaiseAlertGivenSiteIsIsolated() throws Exception {
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader(
                "HTTP/1.1 200 OK\r\n"
                        + "Cross-Origin-Resource-Policy: same-origin\r\n"
                        + "Cross-Origin-Embedder-Policy: require-corp\r\n"
                        + "Cross-Origin-Opener-Policy: same-origin\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(0));
    }

    @Test
    void shouldNotRaiseAlertGivenSiteIsIsolatedWhenSuccessIdentifiedByCustomPage()
            throws Exception {
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader(
                "HTTP/1.1 400 OK\r\n"
                        + "Cross-Origin-Resource-Policy: same-origin\r\n"
                        + "Cross-Origin-Embedder-Policy: require-corp\r\n"
                        + "Cross-Origin-Opener-Policy: same-origin\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(0));
    }

    @Test
    void shouldNotRaiseAlertGivenSiteIsIsolatedWhenSuccessAndIdentifiedByCustomPage()
            throws Exception {
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader(
                "HTTP/1.1 200 OK\r\n"
                        + "Cross-Origin-Resource-Policy: same-origin\r\n"
                        + "Cross-Origin-Embedder-Policy: require-corp\r\n"
                        + "Cross-Origin-Opener-Policy: same-origin\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(0));
    }

    @Test
    void shouldNotRaiseAlertGivenSiteIsNotIsolatedWhenSuccessNotIdentifiedByCustomPage()
            throws Exception {
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader("HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(false);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(0));
    }

    @Test
    void shouldRaiseAlertGivenSiteIsNotIsolatedWhenSuccessIdentifiedByCustomPage()
            throws Exception {
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader("HTTP/1.1 400 OK\r\n" + "Content-Type: text/html\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(3));
    }

    @Test
    void shouldRaiseCorpAlertGivenResponseDoesntSendCorpHeader() throws Exception {
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader(
                "HTTP/1.1 200 OK\r\n"
                        + "Cross-Origin-Embedder-Policy: require-corp\r\n"
                        + "Cross-Origin-Opener-Policy: same-origin\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(1));
        assertThat(
                alertsRaised.get(0).getParam(),
                equalTo(SiteIsolationScanRule.CorpHeaderScanRule.HEADER));
        assertThat(alertsRaised.get(0).getEvidence(), equalTo(""));
    }

    @Test
    void shouldRaiseCorpAlertGivenCorpHeaderIsSetForSameSite() throws Exception {
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader(
                "HTTP/1.1 200 OK\r\n"
                        + "Cross-Origin-Resource-Policy: same-site\r\n"
                        + "Cross-Origin-Embedder-Policy: require-corp\r\n"
                        + "Cross-Origin-Opener-Policy: same-origin\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(1));
        assertThat(
                alertsRaised.get(0).getParam(),
                equalTo(SiteIsolationScanRule.CorpHeaderScanRule.HEADER));
        assertThat(alertsRaised.get(0).getEvidence(), equalTo("same-site"));
    }

    @Test
    void shouldRaiseCorpAlertGivenCorpHeaderContentIsUnexpected() throws Exception {
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader(
                "HTTP/1.1 200 OK\r\n"
                        + "Cross-Origin-Resource-Policy: unexpected\r\n"
                        + "Cross-Origin-Embedder-Policy: require-corp\r\n"
                        + "Cross-Origin-Opener-Policy: same-origin\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(1));
        assertThat(
                alertsRaised.get(0).getParam(),
                equalTo(SiteIsolationScanRule.CorpHeaderScanRule.HEADER));
        assertThat(alertsRaised.get(0).getEvidence(), equalTo("unexpected"));
    }

    @Test
    void shouldRaiseCorpAlertCaseInsensitive() throws Exception {
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader(
                "HTTP/1.1 200 OK\r\n"
                        + "Cross-Origin-Resource-Policy: same-SITE\r\n"
                        + "Cross-Origin-Embedder-Policy: require-corp\r\n"
                        + "Cross-Origin-Opener-Policy: same-origin\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(1));
        assertThat(
                alertsRaised.get(0).getParam(),
                equalTo(SiteIsolationScanRule.CorpHeaderScanRule.HEADER));
        assertThat(alertsRaised.get(0).getEvidence(), equalTo("same-SITE"));
    }

    @Test
    void shouldNotRaiseCorpAlertGivenCorpHeaderIsSetForCrossOrigin() throws Exception {
        // We consider that resource has been explicitly set to be shared.
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader(
                "HTTP/1.1 200 OK\r\n"
                        + "Cross-Origin-Resource-Policy: cross-origin\r\n"
                        + "Cross-Origin-Embedder-Policy: require-corp\r\n"
                        + "Cross-Origin-Opener-Policy: same-origin\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(0));
    }

    @Test
    void shouldRaiseCorpAlertOnlyForSuccessfulQueries() throws Exception {
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader("HTTP/1.1 500 Internal Server Error\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(false);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(0));
    }

    @ParameterizedTest
    @ValueSource(strings = {"Access-Control-Allow-Origin", "access-control-allow-origin"})
    void shouldNotRaiseCorpAlertGivenCorsHeaderIsSet(String corsFieldName) throws Exception {
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader(
                "HTTP/1.1 200 OK\r\n"
                        + corsFieldName
                        + ": *\r\n"
                        + "Cross-Origin-Embedder-Policy: require-corp\r\n"
                        + "Cross-Origin-Opener-Policy: same-origin\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(0));
    }

    @Test
    void shouldRaiseAlertGivenCoepHeaderIsMissing() throws Exception {
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader(
                "HTTP/1.1 200 OK\r\n"
                        + "Content-Type: application/xml\r\n"
                        + "Cross-Origin-Resource-Policy: same-origin\r\n"
                        + "Cross-Origin-Opener-Policy: same-origin\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(1));
        assertThat(
                alertsRaised.get(0).getParam(),
                equalTo(SiteIsolationScanRule.CoepHeaderScanRule.HEADER));
    }

    @Test
    void shouldRaiseAlertGivenCoepHeaderIsNotExpectedValue() throws Exception {
        // Ref: https://html.spec.whatwg.org/multipage/origin.html#the-headers
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader(
                "HTTP/1.1 200 OK\r\n"
                        + "Content-Type: text/html; charset=iso-8859-1\r\n"
                        + "Cross-Origin-Resource-Policy: same-origin\r\n"
                        + "Cross-Origin-Embedder-Policy: unsafe-none\r\n"
                        + "Cross-Origin-Opener-Policy: same-origin\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(1));
        assertThat(
                alertsRaised.get(0).getParam(),
                equalTo(SiteIsolationScanRule.CoepHeaderScanRule.HEADER));
        assertThat(alertsRaised.get(0).getEvidence(), equalTo("unsafe-none"));
    }

    @ParameterizedTest
    @ValueSource(strings = {"require-corp", "credentialless"})
    void shouldNotRaiseAlertGivenCoepHeaderIsAnExpectedValue(String directive) throws Exception {
        // Ref: https://html.spec.whatwg.org/multipage/origin.html#the-headers
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader(
                "HTTP/1.1 200 OK\r\n"
                        + "Content-Type: text/html; charset=iso-8859-1\r\n"
                        + "Cross-Origin-Resource-Policy: same-origin\r\n"
                        + "Cross-Origin-Embedder-Policy: "
                        + directive
                        + "\r\n"
                        + "Cross-Origin-Opener-Policy: same-origin\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, is(empty()));
    }

    @Test
    void shouldRaiseAlertGivenCoopHeaderIsMissing() throws Exception {
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader(
                "HTTP/1.1 200 OK\r\n"
                        + "Content-Type: text/html;charset=utf-8\r\n"
                        + "Cross-Origin-Resource-Policy: same-origin\r\n"
                        + "Cross-Origin-Embedder-Policy: require-corp\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(1));
        assertThat(
                alertsRaised.get(0).getParam(),
                equalTo(SiteIsolationScanRule.CoopHeaderScanRule.HEADER));
    }

    @Test
    void shouldRaiseAlertGivenCoopHeaderIsNotSameOrigin() throws Exception {
        // Ref: https://html.spec.whatwg.org/multipage/origin.html#cross-origin-opener-policies
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader(
                "HTTP/1.1 200 OK\r\n"
                        + "Content-Type: text/html\r\n"
                        + "Cross-Origin-Resource-Policy: same-origin\r\n"
                        + "Cross-Origin-Embedder-Policy: require-corp\r\n"
                        + "Cross-Origin-Opener-Policy: same-origin-allow-popups\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(1));
        assertThat(
                alertsRaised.get(0).getParam(),
                equalTo(SiteIsolationScanRule.CoopHeaderScanRule.HEADER));
        assertThat(alertsRaised.get(0).getEvidence(), equalTo("same-origin-allow-popups"));
    }

    @Test
    void shouldNotRaiseCoepOrCoopAlertGivenResourceIsNotAnHtmlOrXmlDocument() throws Exception {
        // Definition of Document: https://dom.spec.whatwg.org/#document
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader(
                "HTTP/1.1 200 OK\r\n"
                        + "Content-Type: application/json\r\n"
                        + "Cross-Origin-Resource-Policy: same-origin\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(0));
    }

    @Test
    void shouldNotRaiseAlertGivenNoHeaderContentTypeIsPresent() throws Exception {
        // If no header content-type is provided, it is browser-dependent.
        //        It will try to sniff the type.
        // There is a rule ContentTypeMissingScanRule
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader(
                "HTTP/1.1 200 OK\r\n" + "Cross-Origin-Resource-Policy: same-origin\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(0));
    }

    @Test
    void shouldNotRaiseAlertForReportingAPI() throws Exception {
        // Given
        HttpMessage msg = new HttpMessage();
        msg.setRequestHeader("GET / HTTP/1.1");
        msg.setResponseHeader(
                "HTTP/1.1 200 OK\r\n"
                        + "Content-Type: text/html\r\n"
                        + "cross-origin-embedder-policy: require-corp;report-to=\"coep\"\r\n"
                        + "cross-origin-opener-policy: same-origin;report-to=\"coop\"\r\n"
                        + "Cross-Origin-Resource-Policy: same-origin;report-to=\"corp\"\r\n");
        given(passiveScanData.isSuccess(any())).willReturn(true);

        // When
        scanHttpResponseReceive(msg);

        // Then
        assertThat(alertsRaised, hasSize(0));
    }

    @Test
    void shouldReturnExpectedMappings() {
        // Given / When
        Map<String, String> tags = rule.getAlertTags();
        // Then
        assertThat(tags.size(), is(equalTo(2)));
        assertThat(
                tags.containsKey(CommonAlertTag.OWASP_2021_A04_INSECURE_DESIGN.getTag()),
                is(equalTo(true)));
        assertThat(
                tags.containsKey(CommonAlertTag.OWASP_2017_A03_DATA_EXPOSED.getTag()),
                is(equalTo(true)));
        assertThat(
                tags.get(CommonAlertTag.OWASP_2021_A04_INSECURE_DESIGN.getTag()),
                is(equalTo(CommonAlertTag.OWASP_2021_A04_INSECURE_DESIGN.getValue())));
        assertThat(
                tags.get(CommonAlertTag.OWASP_2017_A03_DATA_EXPOSED.getTag()),
                is(equalTo(CommonAlertTag.OWASP_2017_A03_DATA_EXPOSED.getValue())));
    }

    @Test
    void shouldHaveExpectedExampleAlerts() {
        // Given / When
        List<Alert> examples = rule.getExampleAlerts();
        // Then
        assertThat(examples.size(), is(equalTo(3)));
        Alert corp = examples.get(0);
        Alert coep = examples.get(1);
        Alert coop = examples.get(2);
        assertThat(corp.getAlertRef(), is(equalTo("90004-1")));
        assertThat(coep.getAlertRef(), is(equalTo("90004-2")));
        assertThat(coop.getAlertRef(), is(equalTo("90004-3")));
    }

    @Override
    protected SiteIsolationScanRule createScanner() {
        return new SiteIsolationScanRule();
    }
}

Copy link
Member

@kingthorin kingthorin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🎉⭐

@kingthorin kingthorin enabled auto-merge (squash) February 11, 2025 16:24
@kingthorin
Copy link
Member

Done

@Dxk09 Dxk09 requested a review from thc202 February 16, 2025 20:40
@kingthorin kingthorin self-assigned this Feb 19, 2025
Dxk09 and others added 13 commits February 19, 2025 07:46
Added changes made on SiteIsolationScanRule.java

Signed-off-by: Dxk09 <[email protected]>
Add support for 'credentialless' COEP value in Site Isolation Scan (Issue 8840).

Signed-off-by: Dxk09 <[email protected]>
…pscanrulesBeta/SiteIsolationScanRule.java

Co-authored-by: Rick M <[email protected]>
Signed-off-by: Dxk09 <[email protected]>
Removed unit tests to locate them to the right file

Signed-off-by: Dxk09 <[email protected]>
Added tests to right location

Signed-off-by: Dxk09 <[email protected]>
Had lots of duplicate code and decluttered code that I fixed

Signed-off-by: Dxk09 <[email protected]>
removed plus signs

Signed-off-by: Dxk09 <[email protected]>
Last push for the night, will wait for the full copy of the unit test class

Signed-off-by: Dxk09 <[email protected]>
Updated unit tests with correct logic and format

Signed-off-by: Dxk09 <[email protected]>
@kingthorin
Copy link
Member

Rebased and addressed feedback.

@kingthorin kingthorin merged commit 9e707b1 into zaproxy:main Feb 19, 2025
10 checks passed
@thc202
Copy link
Member

thc202 commented Feb 19, 2025

Thank you both!

@github-actions github-actions bot locked and limited conversation to collaborators Feb 19, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
4 participants