SecurityChecker.java

// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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.apache.doris.cloud.security;

import org.apache.doris.common.Config;
import org.apache.doris.common.DdlException;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;

/**
 * SecurityChecker is used to check if the url is safe
 */
public abstract class SecurityChecker {
    private static final Logger LOG = LogManager.getLogger(SecurityChecker.class);

    // Upper bound for one SSRF connectivity probe. The probe only needs the net-hook to observe the
    // resolved IP before the TCP handshake completes; a short timeout keeps CREATE/ALTER responsive when
    // the endpoint is a public-but-unreachable host (that outcome is ignored, see checkEndpointForSsrf).
    private static final int SSRF_PROBE_CONNECT_TIMEOUT_MS = 5000;

    private static volatile SecurityChecker instance;

    protected SecurityChecker() {}

    public static SecurityChecker getInstance() {
        if (instance == null) {
            synchronized (SecurityChecker.class) {
                if (instance == null) {
                    instance = Strings.isNullOrEmpty(Config.security_checker_class_name)
                            ? new DummySecurityChecker() : new UrlSecurityChecker();
                }
            }
        }
        return instance;
    }

    @VisibleForTesting
    public static void setInstanceForTest(SecurityChecker checker) {
        instance = checker;
    }

    /**
     * Check and return safe jdbc url, avoid sql injection or other security issues
     * @param originJdbcUrl
     * @return
     * @throws Exception
     */
    public abstract String getSafeJdbcUrl(String originJdbcUrl) throws Exception;

    /**
     * Install the net-hook for an http(s) request on the current thread, avoid SSRF attack.
     * Only handle http:// https://. Must bracket a real request: startSSRFChecking before issuing it,
     * {@link #stopSSRFChecking()} after it, and no further request may be issued once stopped.
     * @param originUri
     * @throws Exception
     */
    public abstract void startSSRFChecking(String originUri) throws Exception;

    /**
     * Install the net-hook for a raw TCP connection on the current thread, avoid SSRF attack.
     * Handles any protocol that connects through a JDK socket (thrift, jdbc, ...), not only http.
     * Same bracketing contract as {@link #startSSRFChecking(String)}.
     * @throws Exception
     */
    public abstract void startSSRFChecking() throws Exception;

    public abstract void stopSSRFChecking();

    /**
     * Whether a real SSRF checker (the third-party SDK) is installed. When false every
     * {@link #checkEndpointForSsrf(String, int)} is a no-op, so the default open-source build behaves
     * exactly as before.
     */
    public abstract boolean isEnabled();

    /**
     * Point-in-time SSRF gate for a single user-supplied endpoint: brackets a lightweight TCP connect
     * with the no-arg net-hook so the SDK resolves the host and rejects an intranet target before the
     * handshake completes.
     *
     * <p>The SDK signals an intranet target by throwing {@code SSRFUnsafeConnectionException} (a
     * {@link SecurityException}) from inside the connect; that ��� and only that ��� is the rejection. A plain
     * connectivity failure (refused / timeout / unknown host) is NOT an SSRF signal and is ignored, so a
     * catalog that points at a currently-unreachable but public endpoint can still be created.
     */
    public final void checkEndpointForSsrf(String host, int port) throws DdlException {
        if (!isEnabled()) {
            return;
        }
        try {
            startSSRFChecking();
        } catch (Exception e) {
            // isEnabled() is true but the SDK net-hook could not be installed: surface the misconfiguration
            // rather than silently skipping the security gate.
            throw new DdlException("Failed to initialize SSRF checking for endpoint " + host + ":" + port
                    + ": " + e.getMessage(), e);
        }
        try {
            doConnect(host, port, SSRF_PROBE_CONNECT_TIMEOUT_MS);
        } catch (SecurityException e) {
            throw new DdlException("Endpoint " + host + ":" + port
                    + " is rejected by the SSRF policy (intranet address not allowed): " + e.getMessage());
        } catch (IOException e) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("SSRF probe to {}:{} did not complete, ignored (not an SSRF rejection): {}",
                        host, port, e.toString());
            }
        } finally {
            stopSSRFChecking();
        }
    }

    /**
     * The connection seam of {@link #checkEndpointForSsrf(String, int)}: a single real TCP connect the
     * net-hook can observe. Overridable so unit tests can drive the SSRF / unreachable / success branches
     * without the third-party SDK or a live server.
     */
    protected void doConnect(String host, int port, int connectTimeoutMs) throws IOException {
        try (Socket socket = new Socket()) {
            socket.connect(new InetSocketAddress(host, port), connectTimeoutMs);
        }
    }
}