Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
src/main/java/the/bytecode/club/bytecodeviewer/util/ZipUtils.java
### 代码内容：
package the.bytecode.club.bytecodeviewer.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Objects;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * Rudimentary utility class for Zip archives.
 */
public final class ZipUtils {

    // TODO: Maybe migrate to org.apache.commons.compress.archivers.examples.Expander?
    /**
     * Unzip files to path.
     *
     * @param jarPath        the zip file name
     * @param destinationDir the file extract path
     * @throws IOException Signals that an I/O exception has occurred.
     */
    public static void unzipFilesToPath(String jarPath, String destinationDir) throws IOException {
        File file = new File(jarPath);
        try (JarFile jar = new JarFile(file)) {

            // fist get all directories,
            // then make those directory on the destination Path
            /*for (Enumeration<JarEntry> enums = jar.entries(); enums.hasMoreElements(); ) {
                JarEntry entry = (JarEntry) enums.nextElement();

                String fileName = destinationDir + File.separator + entry.getName();
                File f = new File(fileName);

                if (fileName.endsWith("/")) {
                    f.mkdirs();
                }

            }*/

            //now create all files
            for (Enumeration<JarEntry> enums = jar.entries(); enums.hasMoreElements(); ) {
                JarEntry entry = enums.nextElement();

                String fileName = destinationDir + File.separator + entry.getName();
                File f = new File(fileName);

                if (!f.getCanonicalPath().startsWith(destinationDir)) {
                    System.out.println("Zip Slip exploit detected. Skipping entry " + entry.getName());
                    continue;
                }

                File parent = f.getParentFile();
                if (!parent.exists()) {
                    parent.mkdirs();
                }

                if (!fileName.endsWith("/")) {
                    try (InputStream is = jar.getInputStream(entry);
                         FileOutputStream fos = new FileOutputStream(f)) {
                        // write contents of 'is' to 'fos'
                        while (is.available() > 0) {
                            fos.write(is.read());
                        }
                    }
                }
            }
        }
    }

    public static void zipFile(File inputFile, File outputZip) {
        byte[] buffer = new byte[1024];

        try (FileOutputStream fos = new FileOutputStream(outputZip);
             ZipOutputStream zos = new ZipOutputStream(fos)) {
            ZipEntry ze = new ZipEntry(inputFile.getName());
            zos.putNextEntry(ze);
            try (FileInputStream in = new FileInputStream(inputFile)) {
                int len;
                while ((len = in.read(buffer)) > 0) {
                    zos.write(buffer, 0, len);
                }
            }
            zos.closeEntry();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void zipFolder(String srcFolder, String destZipFile, String ignore) throws Exception {
        try (FileOutputStream fileWriter = new FileOutputStream(destZipFile);
             ZipOutputStream zip = new ZipOutputStream(fileWriter)) {
            addFolderToZip("", srcFolder, zip, ignore);
            zip.flush();
        }
    }

    public static void zipFolderAPKTool(String srcFolder, String destZipFile) throws Exception {
        try (FileOutputStream fileWriter = new FileOutputStream(destZipFile);
             ZipOutputStream zip = new ZipOutputStream(fileWriter)) {
            addFolderToZipAPKTool("", srcFolder, zip);
            zip.flush();
        }
    }

    public static void addFileToZip(String path, String srcFile, ZipOutputStream zip, String ignore)
            throws Exception {

        File folder = new File(srcFile);
        if (folder.isDirectory()) {
            addFolderToZip(path, srcFile, zip, ignore);
        } else {
            byte[] buf = new byte[1024];
            int len;
            try (FileInputStream in = new FileInputStream(srcFile)) {
                ZipEntry entry;
                if (ignore == null)
                    entry = new ZipEntry(path + "/" + folder.getName());
                else
                    entry = new ZipEntry(path.replace(ignore, "BCV_Krakatau") + "/" + folder.getName());
                zip.putNextEntry(entry);
                while ((len = in.read(buf)) > 0) {
                    zip.write(buf, 0, len);
                }
            }
        }
    }

    public static void addFileToZipAPKTool(String path, String srcFile, ZipOutputStream zip) throws Exception {
        File folder = new File(srcFile);

        String check = path.toLowerCase();
        //if(check.startsWith("decoded unknown") || check.startsWith("decoded lib") || check.startsWith("decoded
        // assets") || check.startsWith("decoded original") || check.startsWith("decoded smali") || check.startsWith
        // ("decoded apktool.yml"))
        if (check.startsWith("decoded original") || check.startsWith("decoded smali") || check.startsWith("decoded "
                + "apktool.yml"))
            return;

        //if(path.equals("original") || path.equals("classes.dex") || path.equals("apktool.yml"))
        //    continue;

        if (folder.isDirectory()) {
            addFolderToZipAPKTool(path, srcFile, zip);
        } else {
            byte[] buf = new byte[1024];
            int len;
            try (FileInputStream in = new FileInputStream(srcFile)) {
                ZipEntry entry;

                entry = new ZipEntry(path + "/" + folder.getName());
                zip.putNextEntry(entry);

                while ((len = in.read(buf)) > 0) {
                    zip.write(buf, 0, len);
                }
            }
        }
    }

    public static void addFolderToZip(String path, String srcFolder, ZipOutputStream zip, String ignore)
            throws Exception {
        File folder = new File(srcFolder);

        for (String fileName : Objects.requireNonNull(folder.list())) {
            if (path.isEmpty()) {
                addFileToZip(folder.getName(), srcFolder + "/" + fileName, zip, ignore);
            } else {
                addFileToZip(path + "/" + folder.getName(), srcFolder + "/" + fileName, zip, ignore);
            }
        }
    }

    public static void addFolderToZipAPKTool(String path, String srcFolder, ZipOutputStream zip) throws Exception {
        File folder = new File(srcFolder);

        for (String fileName : Objects.requireNonNull(folder.list())) {
            if (path.isEmpty()) {
                addFileToZipAPKTool(folder.getName(), srcFolder + "/" + fileName, zip);
            } else {
                addFileToZipAPKTool(path + "/" + folder.getName(), srcFolder + "/" + fileName, zip);
            }
        }
    }
}

Output:
【漏洞类型】路径遍历漏洞（Zip Slip）

【严重性】高

【漏洞摘要】路径遍历漏洞（也称为Zip Slip）是一种安全漏洞，攻击者可以通过精心构造的压缩文件中的路径名，将文件解压缩到目标系统中未授权的位置。在原始代码的`unzipFilesToPath`方法中，漏洞表现为未验证的压缩文件条目路径名。具体来说，问题出在以下行：

```java
String fileName = destinationDir + File.separator + entry.getName();
File f = new File(fileName);
```

以及：

```java
if (!f.getCanonicalPath().startsWith(destinationDir)) {
    System.out.println("Zip Slip exploit detected. Skipping entry " + entry.getName());
    continue;
}
```

根本原因在于`entry.getName()`可能包含相对路径或上层目录引用（如`../`），导致解压缩的文件可能超出预期的目标目录。即使有安全检查，直接使用`destinationDir`与`entry.getName()`拼接路径仍然存在潜在风险。

【关键代码模式】与此漏洞相关的关键代码模式包括：
- 使用未验证的压缩文件条目路径名进行文件解压缩。
- 直接拼接目标目录和压缩文件条目路径名，未进行充分的路径验证。
- 未验证的用户输入（如果压缩文件路径名来自用户输入）。

【修复方法说明】修复方法通过获取目标目录的规范路径，并在拼接路径名时使用这个规范路径来解决这个问题。具体来说，修复代码中添加了以下行：

```java
String canonicalDestDir = new File(destinationDir).getCanonicalPath();
if (!canonicalDestDir.endsWith(File.separator)) {
    canonicalDestDir += File.separator;
}
```

然后在路径检查时使用`canonicalDestDir`：

```java
if (!f.getCanonicalPath().startsWith(canonicalDestDir)) {
    System.out.println("Zip Slip exploit detected. Skipping entry " + entry.getName());
    continue;
}
```

这种方法确保了压缩文件条目路径名在解压缩时不会超出预期的目标目录，从而防止了路径遍历攻击。最佳实践是始终对文件路径进行验证，并使用规范路径来避免相对路径的攻击。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
src/main/java/the/bytecode/club/bytecodeviewer/util/ZipUtils.java
### 代码内容：
package the.bytecode.club.bytecodeviewer.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Objects;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * Rudimentary utility class for Zip archives.
 */
public final class ZipUtils {

    /**
     * Unzip files to path.
     *
     * @param jarPath        the zip file name
     * @param destinationDir the file extract path
     * @throws IOException Signals that an I/O exception has occurred.
     */
    public static void unzipFilesToPath(String jarPath, String destinationDir) throws IOException {
        File file = new File(jarPath);
        try (JarFile jar = new JarFile(file)) {

            // fist get all directories,
            // then make those directory on the destination Path
            /*for (Enumeration<JarEntry> enums = jar.entries(); enums.hasMoreElements(); ) {
                JarEntry entry = (JarEntry) enums.nextElement();

                String fileName = destinationDir + File.separator + entry.getName();
                File f = new File(fileName);

                if (fileName.endsWith("/")) {
                    f.mkdirs();
                }

            }*/

            //now create all files
            for (Enumeration<JarEntry> enums = jar.entries(); enums.hasMoreElements(); ) {
                JarEntry entry = enums.nextElement();

                String fileName = destinationDir + File.separator + entry.getName();
                File f = new File(fileName);

                File parent = f.getParentFile();
                if (!parent.exists()) {
                    parent.mkdirs();
                }

                if (!fileName.endsWith("/")) {
                    try (InputStream is = jar.getInputStream(entry);
                         FileOutputStream fos = new FileOutputStream(f)) {
                        // write contents of 'is' to 'fos'
                        while (is.available() > 0) {
                            fos.write(is.read());
                        }
                    }
                }
            }
        }
    }

    public static void zipFile(File inputFile, File outputZip) {
        byte[] buffer = new byte[1024];

        try (FileOutputStream fos = new FileOutputStream(outputZip);
             ZipOutputStream zos = new ZipOutputStream(fos)) {
            ZipEntry ze = new ZipEntry(inputFile.getName());
            zos.putNextEntry(ze);
            try (FileInputStream in = new FileInputStream(inputFile)) {
                int len;
                while ((len = in.read(buffer)) > 0) {
                    zos.write(buffer, 0, len);
                }
            }
            zos.closeEntry();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void zipFolder(String srcFolder, String destZipFile, String ignore) throws Exception {
        try (FileOutputStream fileWriter = new FileOutputStream(destZipFile);
             ZipOutputStream zip = new ZipOutputStream(fileWriter)){
            addFolderToZip("", srcFolder, zip, ignore);
            zip.flush();
        }
    }

    public static void zipFolderAPKTool(String srcFolder, String destZipFile) throws Exception {
        try (FileOutputStream fileWriter = new FileOutputStream(destZipFile);
             ZipOutputStream zip = new ZipOutputStream(fileWriter)){
            addFolderToZipAPKTool("", srcFolder, zip);
            zip.flush();
        }
    }

    public static void addFileToZip(String path, String srcFile, ZipOutputStream zip, String ignore)
            throws Exception {

        File folder = new File(srcFile);
        if (folder.isDirectory()) {
            addFolderToZip(path, srcFile, zip, ignore);
        } else {
            byte[] buf = new byte[1024];
            int len;
            try (FileInputStream in = new FileInputStream(srcFile)) {
                ZipEntry entry;
                if (ignore == null)
                    entry = new ZipEntry(path + "/" + folder.getName());
                else
                    entry = new ZipEntry(path.replace(ignore, "BCV_Krakatau") + "/" + folder.getName());
                zip.putNextEntry(entry);
                while ((len = in.read(buf)) > 0) {
                    zip.write(buf, 0, len);
                }
            }
        }
    }

    public static void addFileToZipAPKTool(String path, String srcFile, ZipOutputStream zip) throws Exception {
        File folder = new File(srcFile);

        String check = path.toLowerCase();
        //if(check.startsWith("decoded unknown") || check.startsWith("decoded lib") || check.startsWith("decoded
        // assets") || check.startsWith("decoded original") || check.startsWith("decoded smali") || check.startsWith
        // ("decoded apktool.yml"))
        if (check.startsWith("decoded original") || check.startsWith("decoded smali") || check.startsWith("decoded "
                + "apktool.yml"))
            return;

        //if(path.equals("original") || path.equals("classes.dex") || path.equals("apktool.yml"))
        //    continue;

        if (folder.isDirectory()) {
            addFolderToZipAPKTool(path, srcFile, zip);
        } else {
            byte[] buf = new byte[1024];
            int len;
            try (FileInputStream in = new FileInputStream(srcFile)) {
                ZipEntry entry;

                entry = new ZipEntry(path + "/" + folder.getName());
                zip.putNextEntry(entry);

                while ((len = in.read(buf)) > 0) {
                    zip.write(buf, 0, len);
                }
            }
        }
    }

    public static void addFolderToZip(String path, String srcFolder, ZipOutputStream zip, String ignore)
            throws Exception {
        File folder = new File(srcFolder);

        for (String fileName : Objects.requireNonNull(folder.list())) {
            if (path.isEmpty()) {
                addFileToZip(folder.getName(), srcFolder + "/" + fileName, zip, ignore);
            } else {
                addFileToZip(path + "/" + folder.getName(), srcFolder + "/" + fileName, zip, ignore);
            }
        }
    }

    public static void addFolderToZipAPKTool(String path, String srcFolder, ZipOutputStream zip) throws Exception {
        File folder = new File(srcFolder);

        for (String fileName : Objects.requireNonNull(folder.list())) {
            if (path.isEmpty()) {
                addFileToZipAPKTool(folder.getName(), srcFolder + "/" + fileName, zip);
            } else {
                addFileToZipAPKTool(path + "/" + folder.getName(), srcFolder + "/" + fileName, zip);
            }
        }
    }
}

Output:
【漏洞类型】Zip Slip
【严重性】High
【漏洞摘要】Zip Slip 是一种路径遍历漏洞，攻击者可以通过在压缩文件中包含特殊路径（如 `../`）来在目标系统中写入任意文件。在原始代码中，`unzipFilesToPath` 方法通过将 `jar` 文件中的条目名直接附加到 `destinationDir` 上来创建文件路径，这可能导致攻击者将文件写入到 `destinationDir` 之外的目录中。
【关键代码模式】`unzipFilesToPath` 方法中的代码模式: `String fileName = destinationDir + File.separator + entry.getName();`，以及未验证条目名的代码。
【修复方法说明】修复方法通过检查 `f.getCanonicalPath().startsWith(destinationDir)` 来防止路径遍历攻击，确保文件解压路径在目标目录下。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
documentation/docs/clients/openid-connect.md
### 代码内容：
---
layout: doc
title: OpenID Connect
---

*pac4j* allows you to login using the OpenID Connect protocol v1.0.

It has been tested with various OpenID Connect providers: Google, AzureAD, Okta, IdentityServer3 (and 4), MitreID, Keycloak 4.6...

## 1) Dependency

You need to use the following module: `pac4j-oidc`.

**Example (Maven dependency):**

```xml
<dependency>
    <groupId>org.pac4j</groupId>
    <artifactId>pac4j-oidc</artifactId>
    <version>${pac4j.version}</version>
</dependency>
```

## 2) Clients

### a) Indirect clients

For any OpenID Connect identity provider, you should use the generic [OidcClient](https://github.com/pac4j/pac4j/blob/master/pac4j-oidc/src/main/java/org/pac4j/oidc/client/OidcClient.java) (or one of its subclasses).
It is an indirect client for web browser based authentication.
The configuration is defined via the [`OidcConfiguration`](https://github.com/pac4j/pac4j/blob/master/pac4j-oidc/src/main/java/org/pac4j/oidc/config/OidcConfiguration.java) component.

**Example**:

```java
OidcConfiguration config = new OidcConfiguration();
config.setClientId(clientId);
config.setSecret(secret);
config.setDiscoveryURI(discoveryUri);
OidcClient oidcClient = new OidcClient(config);
```

In some cases (when the discovery url is already known for example), you can use a specific client like for [Google](https://github.com/pac4j/pac4j/blob/master/pac4j-oidc/src/main/java/org/pac4j/oidc/client/GoogleOidcClient.java),
[Azure Active Directory](https://github.com/pac4j/pac4j/blob/master/pac4j-oidc/src/main/java/org/pac4j/oidc/client/AzureAdClient.java), [Keycloak](https://github.com/pac4j/pac4j/blob/master/pac4j-oidc/src/main/java/org/pac4j/oidc/client/KeycloakOidcClient.java)
or [Apple](https://github.com/pac4j/pac4j/blob/master/pac4j-oidc/src/main/java/org/pac4j/oidc/client/AppleClient.java).

**Example**:

```java
OidcConfiguration configuration = new OidcConfiguration();
configuration.setClientId("788339d7-1c44-4732-97c9-134cb201f01f");
configuration.setSecret("we/31zi+JYa7zOugO4TbSw0hzn+hv2wmENO9AS3T84s=");
configuration.setDiscoveryURI("https://login.microsoftonline.com/38c46e5a-21f0-46e5-940d-3ca06fd1a330/.well-known/openid-configuration");
AzureAdClient client = new AzureAdClient(configuration);
```

The `clientId` and `secret` will be provided by the OpenID Connect provider, as well as the `discoveryUri` (to read the metadata of the identity provider). If you do not define the `discoveryUri`, you'll need to provide the provider metadata via the `setProviderMetadata` method.

An [`OidcProfile`](https://github.com/pac4j/pac4j/blob/master/pac4j-oidc/src/main/java/org/pac4j/oidc/profile/OidcProfile.java) is returned after a successful authentication (or one of its subclasses: [`AzureAdProfile`](https://github.com/pac4j/pac4j/blob/master/pac4j-oidc/src/main/java/org/pac4j/oidc/profile/azuread/AzureAdProfile.java), [`GoogleOidcProfile`](https://github.com/pac4j/pac4j/blob/master/pac4j-oidc/src/main/java/org/pac4j/oidc/profile/google/GoogleOidcProfile.java)
or  [`KeycloakOidcProfile`](https://github.com/pac4j/pac4j/blob/master/pac4j-oidc/src/main/java/org/pac4j/oidc/profile/keycloak/KeycloakOidcProfile.java)). All the attributes returned in the ID Token will be available in the `OidcProfile` even if you can get the ID token directly via the `getIdToken()` method.

You can define the flow you want to use via the `setResponseType` and `setResponseMode` methods:

```java
// implicit flow
config.setResponseType("id_token");
config.setResponseMode("form_post");
```

By default, the `response_type` is set to `code` (the authorization code flow) and the `response_mode` is empty.

You can define the scope to use with the `setScope` method:

```java
config.setScope("openid email profile phone");
```

You can request to use the `nonce` parameter to reinforce security via:

```java
config.setUseNonce(true);
```

### b) Direct clients

For direct clients (web services), you can get the `access token` from any OpenID Connect identity provider and use that in your request to get the user profile.

For that, the [HeaderClient](https://github.com/pac4j/pac4j/blob/master/pac4j-http/src/main/java/org/pac4j/http/client/direct/HeaderClient.java) would be appropriate, along with the [UserInfoOidcAuthenticator](https://github.com/pac4j/pac4j/blob/master/pac4j-oidc/src/main/java/org/pac4j/oidc/credentials/authenticator/UserInfoOidcAuthenticator.java).

```java
OidcConfiguration config = new OidcConfiguration();
config.setClientId(clientId);
config.setSecret(secret);
config.setDiscoveryURI(discoveryUri);
UserInfoOidcAuthenticator authenticator = new UserInfoOidcAuthenticator(config);
HeaderClient client = new HeaderClient("Authorization", "Bearer ", authenticator);
```

The request to the server should have an `Authorization` header with the value as `Bearer {access token}`.

## 3) Advanced configuration

You can define how the client credentials (`clientId` and `secret`)  are passed to the token endpoint with the `setClientAuthenticationMethod` method:

```java
config.setClientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
```

When validating the IDToken in the login process, you can set a clock skew:

```java
// 1 minute
config.setMaxClockSkew(60);
```

You can also choose your preferred algorithm to sign the JSON web tokens:

```java
config.setPreferredJwsAlgorithm(JWSAlgorithm.RS256);
```

You can finally set additional parameters by using the `addCustomParam(String key, String value)` method:

```java
// select display mode: page, popup, touch, and wap
config.addCustomParam("display", "popup");
// select prompt mode: none, consent, select_account
config.addCustomParam("prompt", "none");
```

Custom `state` values may be defined in the configuration using the below method:

```java
config.setWithState(true);
config.setStateData("custom-state-value");
```

By default, the local session expires when the access token does, but this can be disabled using:

```java
config.setExpireSessionWithToken(false);
```

The additional param `TokenExpirationAdvance` allows to set the time in seconds, previous to the token expiration, in which the expiration is advanced. By default it is `0` seconds.

```java
config.setTokenExpirationAdvance(10);
```

## 内容2
### 文件路径：
documentation/docs/release-notes.md
### 代码内容：
---
layout: doc
title: Release notes:
---

**v5.2**:

- The JEE core components are now in the `pac4j-jee` dependency (and no longer in the `pac4j-core` dependency)

**v5.1.5**:

- Fixed a NPE on the `SAML2AuthnResponseValidator`
- Added the `setExpiration(Date)` method on the `OidcProfile`
- Fixed the `expireSessionWithToken` behavior for the `OidcProfile`
- Set the `secure` and `httpOnly` flag to `true` for the CSRF cookie
- Fix multi-values attribute duplication in `SAML2Profile`

**v5.1.4**:

- SAML2 configuration can now accept custom socket factories and hostname verification options
- Ability to ignore an existing authentication via the `loadProfilesFromSession` flag at the security filter level.

**v5.1.3**:

- Treat SAML partial logout responses as success

**v5.1.2**:

- Support SAML2 `Scoping` in authentication requests
- `WebContext` is now able to provide the request url directly
- Fix SAML2 response attributes resolution when using ADFS as IdP
- Add claims mapping for the OIDC support.

**v5.1.1**:

- Removed the ORCID OAuth client which no longer works. Use the `OidcClient` instead
- Fixed PKCE OIDC flow support
- Properly parse SAML complex attributes
- For the CAS server OIDC support: allow to disable the checking of authentication attempts

**v5.1.0**:

- SAML2 identity provider metadata resolver can optionally be forced to download the metadata again.
- SAML2 identity provider metadata resolver is given the ability to support `last-modified` attributes for URLs.
- SAML2 response validation can now disable the validation of `authnInstant` by assigning a zero/negative value to
  `SAML2Configuration#configuration.setMaximumAuthenticationLifetime()`. This setting should not be using sparingly.
- Clients can be changed at any time in the `Clients` component

**v5.0.1**:

- Hazelcast-based implementation for SAMLMessageStore
- Added an option to tolerate missing SAML response `Destination` attribute
- SAML support: don't add the friendly name if it is the same as the (mapped) name (avoid duplication of values)
- Improve JWT parsing for nested attributes

**v5.0.0** (see: [what's new in pac4j v5?](/blog/what_s_new_in_pac4j_v5.html)):

- Upgraded to JDK 11
- Removed the `pac4j-saml-opensamlv3` and `pac4j-openid` modules
- Removed deprecated methods and classes
- Removed most generics
- Slightly refactored the auto-initialization
- Refactored the session management (mainly `ProfileManager` and `SessionStore`): reading in the web session does not create it while writing a none-null value in the session always create it. The multi-profile and save-profile-in-session-or-not options can now be defined at the `Client` level, and no longer in the "security filter" and "callback endpoint". The `get(readFromSession)` and `getAll(readFromSession)` methods of the `ProfileManager` are replaced by the `getProfile()` and `getProfiles()` methods
- The SAML central logout does not perform any local logout
- When no authorizers is defined, one of the default authorizers is `isAuthenticated` if the `AnonymousClient` is not used
- Serialize profiles in JSON (instead of using the Java serialization) for the MongoDB, SQL, LDAP and CouchDB `ProfileService` supports; Added a `JsonSerializer` and turned the `JavaSerializationHelper` into a `JavaSerializer`; Removed the `ProfileServiceSerializer`
- Removed the 307 HTTP code for a new POST request after a POST request (use 200 instead)
- Turned the `UserProfile` component into a pure interface and use it as much as possible (especially in the `JwtGenerator` and `JwtAuthenticator`)
- The `ProfileHelper.restoreOrBuildProfile` method has been removed and the behavior is controlled by the `ProfileDefinition` and its `setRestoreProfileFromTypedId` method (enabled for JWT, disabled for others)
- Authorizers and matchers can be defined additionaly with "+"
- CSRF security improvements proposed by Xhelal Likaj (https://github.com/xhlika): longer CSRF token values (32 bytes), CSRF tokens generated per HTTP request and with an internal expiration date (4 hours), CSRF token verification protected against time-based attacks
- Improved responses for unauthenticated users: 401 with "WWW-Authenticate" header or 403 to be compliant with the HTTP spec
- Default authorizers and matchers can be re-defined by users
- Separate the `SessionStore` from the `WebContext`
- Signing operations for SAML2 metadata can now be done using the existing default method or via XMLSec. The choice for the signer component can be decided via `SAML2Configuration`.
- Ability to specify the SAML2 SLO url in the `SAML2Configuration` and metadata.
- Options in `SAML2Configuration` to determine how to URLs should be compared when doing endpoint verifications with SAML2 responses.
- SAML2 logout validation can be given an expected destination so as to not just rely on the SLO endpoint defined in the SAML2 metadata.
- SAML2 requested authentication context class refs are now checked and enforced again in SAML responses.
- The presence of `NameID` elements in SAML2 responses is now made optional, if the `SAML2Configuration` is configured to build the final credential using a SAML2 attribute found in the assertion. If the attribute is not found or is undefined, `NameID` is expected as the default.
- Handle the "same site policy" in cookies (default: `lax`). Renamed `ContextHelper` as `WebContextHelper`
- Authentication requests for protocols that support forced/passive authentication can now be modified on a per-request basis using pre-defined HTTP attributes to control the type of authentication request sent to the provider.

**v4.5.1**:

- Removed the ORCID OAuth client which no longer works. Use the `OidcClient` instead
- Fixed PKCE OIDC flow support

**v4.5.0**:

- SAML2 identity provider metadata resolver can optionally be forced to download the metadata again.
- SAML2 identity provider metadata resolver is given the ability to support `last-modified` attributes for URLs.
- Improve JWT parsing for nested attributes
- Avoid NPE with `null` domain for cookies on `JEEContext`

**v4.4.0**:
- For SAML IdP metadata defined as files, the metadata are reloaded if the file is changed

**v4.3.0**:

- Added a `ChainingConverter` for attributes
- Fix expired access tokens for the OIDC protocol

**v4.2.0**:

- Apple SignIn support (OIDC protocol)
- Serialize profiles in JSON (instead of using the Java serialization) for the MongoDB, SQL, LDAP and CouchDB `ProfileService` supports

**v4.1.0** (see: [what's new in pac4j v4.1?](/blog/what_s_new_in_pac4j_v4_1.html)):

- The `RememberMeAuthorizationGenerator` is deprecated and will be removed in the next version (v5)
- The OpenID support (`YahooOpenIdClient`) is deprecated and will be removed in the next version (v5)
- The `ProfileManagerFactory2` is deprecated and will be removed in the next version (v5)
- Removed the `InternalAttributeHandler`
- The default matchers/authorizers are `securityHeaders`/`none` for web services instead of `csrfToken,securityHeaders`/`csrfCheck` for web applications

**v4.0.3**:

- Fix the expiration date for the SAML generated certificate
- Added a new `ValueRetriever` interface and its implementation `SessionStoreValueRetriever` for the OpenID Connect protocol
- Added support for PKCE to the OpenID Connect protocol [RFC-7636](https://tools.ietf.org/html/rfc7636)
- Improved handling of expired tokens in `OidcProfile`

**v4.0.2**:

- Fix the `ClassCastException` when receiving a SAML logout response
- Send the access token as a header for the `GithubClient`
- CAS front channel logout: fix the 401 error after the logout
- Fix default `CallbackUrlResolver` in `CasClient`

**v4.0.1**:

- Type parameters: add the `? extends Credentials` type parameter for the `Client` in the return type of the method `find` of the `ClientFinder` and add the `UserProfile` type parameter for the `ProfileManager` in the return type of the `getProfileManager` method of the `ProfileManagerFactoryAware*`
- Add setters on `Color`
- Pull the `pac4j-saml-opensamlv3` dependency instead of the `pac4j-saml` dependency
- Remove deprecated behaviors: the retrieval of one `CommonProfile` in request or session via the `ProfileManager` and the retrieval of a `String` as the requested URL
- The default client name parameter used for security has a new value (`force_client`) to avoid conflicting with the default client name parameter (`client_name`) used on the callback (the old value is still used as a fallback, but will be removed)
- Allow `pac4j-saml` to store and generate SAML metadata and keystores using a REST API and provide options for extensibility so custom components can be designed and injected to manage metadata artifacts externally. Resolution of SAML2 identity provider metadata can be controlled/overridden.
- Handle a strange use case for the `JEEContext` where the `request.getRequestURI()` returns a path starting by a double slash
- Can return a custom profile when the authentication fails or is cancelled ("silent login")
- Fix the CAS logout URL computation (for central logout without prefix)
- Introduce the `WebContextFactory` concept and the `JEEContextFactory` implementation

**v4.0.0**:

- Improved the profile manager configuration
- Renamed `J2E` components as `JEE`
- Started updating dependencies via Renovate
- A client can return any kind of profile (using a custom `AuthorizationGenerator` or `ProfileCreator`) and even a minimal user profile (`UserProfile`)
- HTTP actions are no longer applied automatically to the web context (the `setResponseStatus` and `writeResponseContent` methods have been removed from the `WebContext` interface), an `HttpActionAdapter` must be used for that. Multiple HTTP actions (inheriting from `HttpAction`) are created to handle the necessary HTTP actions. The `RedirectAction` is replaced by the new HTTP actions inheriting from `RedirectionAction`. The `redirect` method is renamed as `getRedirectionAction`
- By default, the CSRF check applies on the PUT, PATCH and DELETE requests in addition to the POST requests
- Renamed the `SAMLMessageStorage*` classes as `SAMLMessageStore*` (based on `Store`)
- For `Google2Client`, change profile URL from `https://www.googleapis.com/plus/v1/people/me` to `https://www.googleapis.com/oauth2/v3/userinfo`. This change is to prepare for the shutdown of Google plus API. This change will remove the `birthday` and `emails` attribute for `Google2Client`.
- For an AJAX request, only generates the redirection URL when requested (`addRedirectionUrlAsHeader` property of the `DefaultAjaxRequestResolver`)
- Updated the APIs to use `Optional` instead of returning `null`
- Use the 303 "See Other" and 307 "Temporary Redirect" HTTP actions after a POST request (`RedirectionActionHelper`)
- Handles originally requested URLs with POST method
- Add HTTP POST Simple-Sign protocol implementation
- Properly handle states and nonces for multiple OIDC clients
- A profile can be renewed by its client when it's expired
- Most web authorizers are now matchers. The default matchers are "securityHeaders,csrfToken" and the default authorizer is "csrfCheck". Use "none" for no matcher or authorizer
- Use the `FindBest` utility class to find the best adapter, logic...
- Support for the OIDC back-channel and front-channel logouts
- Load the profiles in the `ProfileManager` (from the session or not) like in the `DefaultSecurityLogic` via the `getLikeDefaultSecurityLogic` and `getAllLikeDefaultSecurityLogic` methods
- REVERT: remove the ID token in the `removeLoginData`  method (previously `clearSensitiveData`)
- The `pac4j-saml` module is saved as the legacy `pac4j-saml-opensamlv3` module and upgraded to JDK 11 and OpenSAML v4

---

 The 3.x stream is no longer maintained except via the LTS program.

**v3.9.0**:

- Serialize profiles in JSON (instead of using the Java serialization) for the MongoDB, SQL, LDAP and CouchDB `ProfileService` supports

**v3.8.3**:

- Upgrade the nimbus-jose-jwt library to version 7.9 because of [CVE-2019-17195](https://connect2id.com/blog/nimbus-jose-jwt-7-9)

**v3.8.2**:

- Add customizable SAML post Logout URL
- QualifiedName must not be included by default in SAML authentication requests
- Added replay protectection to the SAML client.
- Fix SAML signature validation w.r.t. WantAssertionsSigned handling. Signing is now always required, even when WantAssertionsSigned is disabled. WantAssertionsSigned now requires explicit signing of the assertions, not the response.
- Added support for the SAML artifact binding for the authentication response.
- Sign metadata when configured to do so and open up the metadata generation API for customization.
- Never sign AuthnRequests with XMLSig when using REDIRECT binding, signing is done via the Signature query parameter.
- Added support for LinkedIn v2 API
- Added support for FigShare

**v3.7.0**:

- Fix SAML SP metadata signature
- CAS improvements: better service requests detection, support of the CAS server `method` parameter
- Fix the `CasRestProfile` with JWT
- Add HTTP POST Simple-Sign protocol implementation
- Add the `get`, `post`, `put` and `delete` matchers based on the `HttpMethodMatcher` when not defined

**v3.6.1**:

- Fix Google OAuth support

**v3.6.0**:

- Multiple authn context class refs can be set in the SAML protocol support
- For `Google2Client`, change profile url from `https://www.googleapis.com/plus/v1/people/me` to `https://www.googleapis.com/oauth2/v3/userinfo`. This change is to prepare for the shutdown of Google plus API. This change will make the `birthday` attribute return `null` and `emails` attribute resolve a single email from `email` attribute for `Google2Client`.
- Clean shutdown of the `SAML2Client` via the `destroy` method
- Do not clear the ID Token as a sensitive data in the `OidcProfile`
- Improve signature and digest methods for SAML metadata
- Enhance OAuth 2 generic support
- Use the NameID as the fallback of the SessionIndex for the SAML SLO support

**v3.5.0**:

- Added `UserInfoOidcAuthenticator` to authenticate a user based on an access token received from an OpenID Connect login process
- Updated the OpenID Connect/JWT dependencies (v6)
- Added `DirectBearerAuthClient`
- Handled the inResponseTo and the RelayState in the logout response (SAML)
- Added `trustedClasses` to the `JavaSerializationHelper`

**v3.4.0**:

- Added ability to create a composition of authorizers (conjunction or disjunction)
- SAML SLO support with SOAP (ingoing only), HTTP-POST and HTTP-Redirect bindings
- OpenID Connect improvements: supports multiple JWS algorithms from the identity server, retrieves the Keycloak roles

**v3.3.0**:

- Improve SAML support: always return a default key which is a private one, add a SingleLogoutService URL in the SP metadata, make local and central logouts work together, allow attributes to be mapped to new names
- Default state generation can be overriden by your own `StateGenerator` for the OAuth, OpenID Connect and SAML protocols
- Custom OpenSAML bootstrapping in SAML authentication module
- X509 certificate support

**v3.2.0**:

- Allow to set the `profileId` for the `GenericOAuth20Client`
- Fixed the `setConfiguration` method name in the OAuth v2.0 support
- Optionally sign or specificy requested attributes in the SAML SP metadata
- Update to Scribejava v5.6.0
- Added support for HiOrg-Server (OAuth)
- Revised OAuth error handling for extracting user profiles. Now, an exception is thrown instead of returning an empty profile
- Fix the `Access-Control-Expose-Headers` name and the `Access-Control-Allow-Credentials` header verification

**v3.1.0**:

- Added attribute merging capabilities for the user profile: multiple attributes with the same name and value of the collection type can be merged into a single attribute
- Added Weibo, QQ and Wechat (OAuth) supports

**v3.0.3:**

- `AzureAdClient` uses the `PathParameterCallbackUrlResolver` by default

**v3.0.2**:

- Properly handles all the HTTP codes in the `setResponseStatus` method of the `J2EContext`
- Added the `setExcludedPath` and `setExcludedPattern` methods to the `PathMatcher` class (for Shiro)

**v3.0.1**:

- The `ProfileHelper.flatIntoOneProfile` method returns an `AnonymousProfile` (instead of empty) if it's the only profile

**v3.0.0**:

- Handle AJAX requests in the OpenID Connect logout
- All session interactions are done via the `SessionStore` (retrieved from the `WebContext`)
- All exceptions (especially `HttpAction`) are unchecked
- Upgraded dependencies
- Added "multi-tenancy" capabilities: you can dynamically define multiple callback URLs for the same client, except for SAML for which you need as many `SAML2Client` as the number of different callback URLs you want
- The `CallbackUrlResolver` computes the callback URL (using a query parameter or a path parameter to define the client), based on the `UrlResolver` which computes a URL
- You can define an error (page) URL at the logics' level to handle unexpected exceptions
- The SAML Keystore alias can be defined via a property; SAML date comparisons are now UTC-based
- The client name is not set at the credential's level
- The username of the `AzureAdProfile` is the UPN
- The issue time is generated and an expiration date can be used for a JWT
- The OpenID Connect user profile can be expired
- In the `J2EContext`, header names are checked in a case-insensitive way
- Supports the `javax.faces.partial.ajax` parameter for AJAX requests
- If only one client is defined in the configuration, it is used as a fallback on the security and callback endpoints

[▸ Older versions...](release-notes-older.html)

## 内容3
### 文件路径：
pac4j-oidc/src/main/java/org/pac4j/oidc/config/OidcConfiguration.java
### 代码内容：
package org.pac4j.oidc.config;

import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.pac4j.core.client.config.BaseClientConfiguration;
import org.pac4j.core.context.HttpConstants;
import org.pac4j.core.exception.TechnicalException;
import org.pac4j.core.logout.handler.DefaultLogoutHandler;
import org.pac4j.core.logout.handler.LogoutHandler;
import org.pac4j.core.util.generator.ValueGenerator;
import org.pac4j.core.util.generator.RandomValueGenerator;

import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.util.DefaultResourceRetriever;
import com.nimbusds.jose.util.ResourceRetriever;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.ResponseType;
import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod;
import com.nimbusds.openid.connect.sdk.OIDCResponseTypeValue;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import org.pac4j.oidc.util.SessionStoreValueRetriever;
import org.pac4j.oidc.util.ValueRetriever;
import org.pac4j.oidc.profile.creator.TokenValidator;

import static org.pac4j.core.util.CommonHelper.*;

/**
 * OpenID Connect configuration.
 *
 * @author Jerome Leleu
 * @since 1.9.2
 */
public class OidcConfiguration extends BaseClientConfiguration {

    public static final String SCOPE = "scope";
    public static final String CUSTOM_PARAMS = "custom_params";
    public static final String RESPONSE_TYPE = "response_type";
    public static final String RESPONSE_MODE = "response_mode";
    public static final String REDIRECT_URI = "redirect_uri";
    public static final String CLIENT_ID = "client_id";
    public static final String STATE = "state";
    public static final String MAX_AGE = "max_age";
    public static final String PROMPT = "prompt";
    public static final String NONCE = "nonce";
    public static final String CODE_CHALLENGE = "code_challenge";
    public static final String CODE_CHALLENGE_METHOD = "code_challenge_method";

    public static final List<ResponseType> AUTHORIZATION_CODE_FLOWS = Collections
        .unmodifiableList(Arrays.asList(new ResponseType(ResponseType.Value.CODE)));
    public static final List<ResponseType> IMPLICIT_FLOWS = Collections
        .unmodifiableList(Arrays.asList(new ResponseType(OIDCResponseTypeValue.ID_TOKEN),
            new ResponseType(OIDCResponseTypeValue.ID_TOKEN, ResponseType.Value.TOKEN)));
    public static final List<ResponseType> HYBRID_CODE_FLOWS = Collections.unmodifiableList(Arrays.asList(
        new ResponseType(ResponseType.Value.CODE, OIDCResponseTypeValue.ID_TOKEN),
        new ResponseType(ResponseType.Value.CODE, ResponseType.Value.TOKEN),
        new ResponseType(ResponseType.Value.CODE, OIDCResponseTypeValue.ID_TOKEN, ResponseType.Value.TOKEN)));

    /* default max clock skew */
    public static final int DEFAULT_MAX_CLOCK_SKEW = 30;

    /* default time period advance (in seconds) for considering an access token expired */
    public static final int DEFAULT_TOKEN_EXPIRATION_ADVANCE = 0;

    /* OpenID client identifier */
    private String clientId;

    /* OpenID secret */
    private String secret;

    /* discovery URI for fetching OP metadata (http://openid.net/specs/openid-connect-discovery-1_0.html) */
    private String discoveryURI;

    /* Scope */
    private String scope;

    /* Map containing user defined parameters */
    private Map<String, String> customParams = new HashMap<>();

    /* client authentication method used at token End Point */
    private ClientAuthenticationMethod clientAuthenticationMethod;

    /* use nonce? */
    private boolean useNonce;

    /* disable PKCE, even when supported by the IdP */
    private boolean disablePkce = false;

    /* use PKCE, when null, lookup support from metadata */
    private CodeChallengeMethod pkceMethod;

    /* Preferred JWS algorithm */
    private JWSAlgorithm preferredJwsAlgorithm;

    /* max_age seconds since the last time the End-User was actively authenticated by the OP */
    private Integer maxAge;

    /* max clock skew in seconds */
    private int maxClockSkew = DEFAULT_MAX_CLOCK_SKEW;

    private ResourceRetriever resourceRetriever;

    private OIDCProviderMetadata providerMetadata;

    private ResponseType responseType = AUTHORIZATION_CODE_FLOWS.get(0);

    private String responseMode;

    private String logoutUrl;

    private int connectTimeout = HttpConstants.DEFAULT_CONNECT_TIMEOUT;

    private int readTimeout = HttpConstants.DEFAULT_READ_TIMEOUT;

    private boolean withState = true;

    private Map<String, String> mappedClaims = new LinkedHashMap<>();

    private ValueGenerator stateGenerator = new RandomValueGenerator();

    private ValueGenerator codeVerifierGenerator = new RandomValueGenerator(50);

    private ValueRetriever valueRetriever = new SessionStoreValueRetriever();

    /* checks if sessions expire with token expiration (see also `tokenExpirationAdvance`) */
    private boolean expireSessionWithToken = true;

    /** time period advance (in seconds) for considering an access token expired */
    private int tokenExpirationAdvance = DEFAULT_TOKEN_EXPIRATION_ADVANCE;

    private LogoutHandler logoutHandler;

    private TokenValidator tokenValidator;

    @Override
    protected void internalInit() {
        // checks
        assertNotBlank("clientId", getClientId());
        if (!AUTHORIZATION_CODE_FLOWS.contains(responseType) && !IMPLICIT_FLOWS.contains(responseType)
            && !HYBRID_CODE_FLOWS.contains(responseType)) {
            throw new TechnicalException("Unsupported responseType: " + responseType);
        }
        // except for the implicit flow and when PKCE is disabled, the secret is mandatory
        if (!IMPLICIT_FLOWS.contains(responseType) && isDisablePkce()) {
            assertNotBlank("secret", getSecret());
        }
        if (this.getDiscoveryURI() == null && this.getProviderMetadata() == null) {
            throw new TechnicalException("You must define either the discovery URL or directly the provider metadata");
        }

        // default value
        if (getResourceRetriever() == null) {
            setResourceRetriever(new DefaultResourceRetriever(getConnectTimeout(),getReadTimeout()));
        }
        if (this.getProviderMetadata() == null) {
            assertNotBlank("discoveryURI", getDiscoveryURI());
            try {
                // Download OIDC metadata
                this.setProviderMetadata(OIDCProviderMetadata.parse(getResourceRetriever().retrieveResource(
                    new URL(this.getDiscoveryURI())).getContent()));
            } catch (final IOException | ParseException e) {
                throw new TechnicalException(e);
            }
        }
        if (this.logoutHandler == null) {
            this.logoutHandler = new DefaultLogoutHandler();
        }
    }

    public OIDCProviderMetadata getProviderMetadata() {
        return this.providerMetadata;
    }

    public OIDCProviderMetadata findProviderMetadata() {
        init();

        return this.providerMetadata;
    }

    public void setProviderMetadata(final OIDCProviderMetadata providerMetadata) {
        this.providerMetadata = providerMetadata;
    }

    public String getClientId() {
        return clientId;
    }

    public void setClientId(final String clientId) {
        this.clientId = clientId;
    }

    public String getSecret() {
        return secret;
    }

    public void setSecret(final String secret) {
        this.secret = secret;
    }

    public String getDiscoveryURI() {
        return discoveryURI;
    }

    public void defaultDiscoveryURI(final String discoveryURI) {
        if (this.discoveryURI == null) {
            this.discoveryURI = discoveryURI;
        }
    }

    public String getScope() {
        return this.scope;
    }

    public void setScope(final String scope) {
        this.scope = scope;
    }

    public Map<String, String> getCustomParams() {
        return customParams;
    }

    public String getCustomParam(String name) {
        return customParams.get(name);
    }

    public void setCustomParams(final Map<String, String> customParams) {
        assertNotNull("customParams", customParams);
        this.customParams = customParams;
    }

    public void addCustomParam(final String key, final String value) {
        this.customParams.put(key, value);
    }

    public ClientAuthenticationMethod getClientAuthenticationMethod() {
        return clientAuthenticationMethod;
    }

    public void setClientAuthenticationMethod(final ClientAuthenticationMethod clientAuthenticationMethod) {
        this.clientAuthenticationMethod = clientAuthenticationMethod;
    }

    public void setClientAuthenticationMethodAsString(final String auth) {
        this.clientAuthenticationMethod = ClientAuthenticationMethod.parse(auth);
    }

    public boolean isUseNonce() {
        return useNonce;
    }

    public void setUseNonce(final boolean useNonce) {
        this.useNonce = useNonce;
    }

    public boolean isDisablePkce() {
        return disablePkce;
    }

    public void setDisablePkce(boolean disablePkce) {
        this.disablePkce = disablePkce;
    }

    public CodeChallengeMethod findPkceMethod() {
        init();

        if (isDisablePkce()) {
            return null;
        }
        if (getPkceMethod() == null) {
            if (getProviderMetadata() == null) {
                return null;
            }
            var methods = getProviderMetadata().getCodeChallengeMethods();
            if (methods == null || methods.isEmpty()) {
                return null;
            }
            if (methods.contains(CodeChallengeMethod.S256)) {
                return CodeChallengeMethod.S256;
            }
            return methods.get(0);
        }
        return getPkceMethod();
    }

    public CodeChallengeMethod getPkceMethod() {
        return pkceMethod;
    }

    public void setPkceMethod(CodeChallengeMethod pkceMethod) {
        this.pkceMethod = pkceMethod;
    }

    public JWSAlgorithm getPreferredJwsAlgorithm() {
        return preferredJwsAlgorithm;
    }

    public void setPreferredJwsAlgorithm(final JWSAlgorithm preferredJwsAlgorithm) {
        this.preferredJwsAlgorithm = preferredJwsAlgorithm;
    }

    public void setPreferredJwsAlgorithmAsString(final String preferredJwsAlgorithm) {
        this.preferredJwsAlgorithm = JWSAlgorithm.parse(preferredJwsAlgorithm);
    }

    public Integer getMaxAge() {
        return maxAge;
    }

    public void setMaxAge(final Integer maxAge) {
        this.maxAge = maxAge;
    }

    public int getMaxClockSkew() {
        return maxClockSkew;
    }

    public void setMaxClockSkew(final int maxClockSkew) {
        this.maxClockSkew = maxClockSkew;
    }

    public int getConnectTimeout() {
        return connectTimeout;
    }

    public void setConnectTimeout(final int connectTimeout) {
        this.connectTimeout = connectTimeout;
    }

    public int getReadTimeout() {
        return readTimeout;
    }

    public void setReadTimeout(final int readTimeout) {
        this.readTimeout = readTimeout;
    }

    public void configureHttpRequest(HTTPRequest request) {
        request.setConnectTimeout(getConnectTimeout());
        request.setReadTimeout(getReadTimeout());
    }

    public ResourceRetriever getResourceRetriever() {
        return resourceRetriever;
    }

    public ResourceRetriever findResourceRetriever() {
        init();

        return resourceRetriever;
    }

    public void setDiscoveryURI(final String discoveryURI) {
        this.discoveryURI = discoveryURI;
    }

    public void setResourceRetriever(final ResourceRetriever resourceRetriever) {
        this.resourceRetriever = resourceRetriever;
    }

    public String getResponseType() {
        return responseType.toString();
    }

    public void setResponseType(final String responseType) {
        try {
            this.responseType = ResponseType.parse(responseType);
        } catch (ParseException e) {
            throw new TechnicalException("Unrecognised responseType: " + responseType, e);
        }
    }

    public String getResponseMode() {
        return responseMode;
    }

    public void setResponseMode(final String responseMode) {
        this.responseMode = responseMode;
    }

    public String findLogoutUrl() {
        init();

        if(logoutUrl == null && getProviderMetadata().getEndSessionEndpointURI() != null) {
            return getProviderMetadata().getEndSessionEndpointURI().toString();
        }
        return logoutUrl;
    }

    public String getLogoutUrl() {
        return logoutUrl;
    }

    public void setLogoutUrl(final String logoutUrl) {
        this.logoutUrl = logoutUrl;
    }

    public boolean isWithState() {
        return withState;
    }

    public void setWithState(final boolean withState) {
        this.withState = withState;
    }

    public boolean isExpireSessionWithToken() {
        return expireSessionWithToken;
    }

    public void setExpireSessionWithToken(boolean expireSessionWithToken) {
        this.expireSessionWithToken = expireSessionWithToken;
    }

    public int getTokenExpirationAdvance() {
        return isExpireSessionWithToken() ? tokenExpirationAdvance : -1;
    }

    public void setTokenExpirationAdvance(int tokenExpirationAdvance) {
        this.tokenExpirationAdvance = tokenExpirationAdvance;
    }

    public ValueGenerator getStateGenerator() {
        return stateGenerator;
    }

    public void setStateGenerator(final ValueGenerator stateGenerator) {
        assertNotNull("stateGenerator", stateGenerator);
        this.stateGenerator = stateGenerator;
    }

    public ValueGenerator getCodeVerifierGenerator() {
        return codeVerifierGenerator;
    }

    public void setCodeVerifierGenerator(ValueGenerator codeVerifierGenerator) {
        assertNotNull("codeVerifierGenerator", codeVerifierGenerator);
        this.codeVerifierGenerator = codeVerifierGenerator;
    }

    public ValueRetriever getValueRetriever() {
        return valueRetriever;
    }

    public void setValueRetriever(ValueRetriever valueRetriever) {
        assertNotNull("valueRetriever", valueRetriever);
        this.valueRetriever = valueRetriever;
    }

    public LogoutHandler findLogoutHandler() {
        init();

        return logoutHandler;
    }

    public void setLogoutHandler(final LogoutHandler logoutHandler) {
        this.logoutHandler = logoutHandler;
    }

    public TokenValidator getTokenValidator() {
        return tokenValidator;
    }

    public void setTokenValidator(final TokenValidator tokenValidator) {
        this.tokenValidator = tokenValidator;
    }

    public TokenValidator findTokenValidator() {
        if (this.tokenValidator == null) {
            setTokenValidator(new TokenValidator(this));
        }
        return tokenValidator;
    }

    public Map<String, String> getMappedClaims() {
        return mappedClaims;
    }

    public void setMappedClaims(Map<String, String> mappedClaims) {
        this.mappedClaims = mappedClaims;
    }

    @Override
    public String toString() {
        return toNiceString(this.getClass(), "clientId", clientId, "secret", "[protected]",
            "discoveryURI", discoveryURI, "scope", scope, "customParams", customParams,
            "clientAuthenticationMethod", clientAuthenticationMethod, "useNonce", useNonce,
            "preferredJwsAlgorithm", preferredJwsAlgorithm, "maxAge", maxAge, "maxClockSkew", maxClockSkew,
            "connectTimeout", connectTimeout, "readTimeout", readTimeout, "resourceRetriever", resourceRetriever,
            "responseType", responseType, "responseMode", responseMode, "logoutUrl", logoutUrl,
            "withState", withState, "stateGenerator", stateGenerator, "logoutHandler", logoutHandler,
            "tokenValidator", tokenValidator, "mappedClaims", mappedClaims);
    }
}

## 内容4
### 文件路径：
pac4j-oidc/src/main/java/org/pac4j/oidc/credentials/authenticator/UserInfoOidcAuthenticator.java
### 代码内容：
package org.pac4j.oidc.credentials.authenticator;

import static java.util.Optional.ofNullable;
import static org.pac4j.core.profile.AttributeLocation.PROFILE_ATTRIBUTE;

import java.io.IOException;
import java.util.Map;

import javax.naming.AuthenticationException;

import org.pac4j.core.context.WebContext;
import org.pac4j.core.context.session.SessionStore;
import org.pac4j.core.credentials.Credentials;
import org.pac4j.core.credentials.TokenCredentials;
import org.pac4j.core.credentials.authenticator.Authenticator;
import org.pac4j.core.exception.TechnicalException;
import org.pac4j.core.util.CommonHelper;
import org.pac4j.core.util.InitializableObject;
import org.pac4j.oidc.config.OidcConfiguration;
import org.pac4j.oidc.profile.OidcProfile;
import org.pac4j.oidc.profile.OidcProfileDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
import com.nimbusds.openid.connect.sdk.UserInfoRequest;
import com.nimbusds.openid.connect.sdk.UserInfoResponse;
import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;

/**
 * The OpenId Connect authenticator by user info.
 *
 * @author Rakesh Sarangi
 * @since 3.5.0
 */
public class UserInfoOidcAuthenticator extends InitializableObject implements Authenticator {

    private static final Logger logger = LoggerFactory.getLogger(UserInfoOidcAuthenticator.class);

    private OidcConfiguration configuration;

    public UserInfoOidcAuthenticator() {}

    public UserInfoOidcAuthenticator(final OidcConfiguration configuration) {
        this.configuration = configuration;
    }

    @Override
    protected void internalInit() {
        CommonHelper.assertNotNull("configuration", configuration);
    }

    @Override
    public void validate(final Credentials cred, final WebContext context, final SessionStore sessionStore) {
        init();

        final var credentials = (TokenCredentials) cred;
        final var profileDefinition = new OidcProfileDefinition();
        final var profile = (OidcProfile) profileDefinition.newProfile();
        final var accessToken = new BearerAccessToken(credentials.getToken());
        profile.setAccessToken(accessToken);
        final var userInfoClaimsSet = fetchOidcProfile(accessToken);
        ofNullable(userInfoClaimsSet)
            .map(JWTClaimsSet::getClaims)
            .ifPresent(claims -> collectProfileClaims(profileDefinition, profile, claims));

        // session expiration with token behavior
        profile.setTokenExpirationAdvance(configuration.getTokenExpirationAdvance());

        credentials.setUserProfile(profile);
    }

    protected void collectProfileClaims(final OidcProfileDefinition profileDefinition,
                                        final OidcProfile profile,
                                        final Map<String, Object> claims) {
        claims.forEach((name, value) -> {
            if (configuration.getMappedClaims().containsKey(name)) {
                var actualName = configuration.getMappedClaims().get(name);
                logger.debug("Mapping claim {} as {} with values {} to profile", name, actualName, value);
                profileDefinition.convertAndAdd(profile, PROFILE_ATTRIBUTE, actualName, value);
            } else {
                logger.debug("Adding claim {} to profile with values {}", name, value);
                profileDefinition.convertAndAdd(profile, PROFILE_ATTRIBUTE, name, value);
            }
        });
    }

    private JWTClaimsSet fetchOidcProfile(BearerAccessToken accessToken) {
        final var userInfoRequest = new UserInfoRequest(configuration.findProviderMetadata().getUserInfoEndpointURI(),
            accessToken);
        final var userInfoHttpRequest = userInfoRequest.toHTTPRequest();
        configuration.configureHttpRequest(userInfoHttpRequest);
        try {
            final var httpResponse = userInfoHttpRequest.send();
            logger.debug("Token response: status={}, content={}", httpResponse.getStatusCode(),
                httpResponse.getContent());
            final var userInfoResponse = UserInfoResponse.parse(httpResponse);
            if (userInfoResponse instanceof UserInfoErrorResponse) {
                logger.error("Bad User Info response, error={}",
                    ((UserInfoErrorResponse) userInfoResponse).getErrorObject().toJSONObject());
                throw new AuthenticationException();
            } else {
                final var userInfoSuccessResponse = (UserInfoSuccessResponse) userInfoResponse;
                final JWTClaimsSet userInfoClaimsSet;
                if (userInfoSuccessResponse.getUserInfo() != null) {
                    userInfoClaimsSet = userInfoSuccessResponse.getUserInfo().toJWTClaimsSet();
                } else {
                    userInfoClaimsSet = userInfoSuccessResponse.getUserInfoJWT().getJWTClaimsSet();
                }
                return userInfoClaimsSet;
            }
        } catch (IOException | ParseException | java.text.ParseException | AuthenticationException e) {
            throw new TechnicalException(e);
        }
    }

    public OidcConfiguration getConfiguration() {
        return configuration;
    }

    public void setConfiguration(final OidcConfiguration configuration) {
        this.configuration = configuration;
    }
}

## 内容5
### 文件路径：
pac4j-oidc/src/main/java/org/pac4j/oidc/profile/creator/TokenValidator.java
### 代码内容：
package org.pac4j.oidc.profile.creator;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.proc.BadJOSEException;
import com.nimbusds.jwt.JWT;
import com.nimbusds.oauth2.sdk.auth.Secret;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.openid.connect.sdk.Nonce;
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;
import org.pac4j.core.exception.TechnicalException;
import org.pac4j.core.util.CommonHelper;
import org.pac4j.oidc.config.OidcConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.List;

/**
 * ID Token validator.
 *
 * @author Jerome Leleu
 * @since 3.4.0
 */
public class TokenValidator {

    protected final Logger logger = LoggerFactory.getLogger(getClass());

    private final List<IDTokenValidator> idTokenValidators;

    public TokenValidator(final OidcConfiguration configuration) {

        // check algorithms
        final var metadataAlgorithms = configuration.findProviderMetadata().getIDTokenJWSAlgs();
        CommonHelper.assertTrue(CommonHelper.isNotEmpty(metadataAlgorithms),
            "There must at least one JWS algorithm supported on the OpenID Connect provider side");
        List<JWSAlgorithm> jwsAlgorithms = new ArrayList<>();
        final var preferredAlgorithm = configuration.getPreferredJwsAlgorithm();
        if (metadataAlgorithms.contains(preferredAlgorithm)) {
            jwsAlgorithms.add(preferredAlgorithm);
        } else {
            jwsAlgorithms = metadataAlgorithms;
            logger.warn("Preferred JWS algorithm: {} not available. Using all metadata algorithms: {}",
                preferredAlgorithm, metadataAlgorithms);
        }

        idTokenValidators = new ArrayList<>();
        final var _clientID = new ClientID(configuration.getClientId());

        for (var jwsAlgorithm : jwsAlgorithms) {
            if ("none".equals(jwsAlgorithm.getName())) {
                jwsAlgorithm = null;
            }

            // build validator
            final IDTokenValidator idTokenValidator;
            if (jwsAlgorithm == null) {
                idTokenValidator = new IDTokenValidator(configuration.findProviderMetadata().getIssuer(), _clientID);
            } else if (CommonHelper.isNotBlank(configuration.getSecret()) && (JWSAlgorithm.HS256.equals(jwsAlgorithm) ||
                JWSAlgorithm.HS384.equals(jwsAlgorithm) || JWSAlgorithm.HS512.equals(jwsAlgorithm))) {
                final var _secret = new Secret(configuration.getSecret());
                idTokenValidator = createHMACTokenValidator(configuration, jwsAlgorithm, _clientID, _secret);
            } else {
                idTokenValidator = createRSATokenValidator(configuration, jwsAlgorithm, _clientID);
            }
            idTokenValidator.setMaxClockSkew(configuration.getMaxClockSkew());

            idTokenValidators.add(idTokenValidator);
        }
    }

    protected IDTokenValidator createRSATokenValidator(final OidcConfiguration configuration,
                                                       final JWSAlgorithm jwsAlgorithm, final ClientID clientID) {
        try {
            return new IDTokenValidator(configuration.findProviderMetadata().getIssuer(), clientID, jwsAlgorithm,
                configuration.findProviderMetadata().getJWKSetURI().toURL(), configuration.findResourceRetriever());
        } catch (final MalformedURLException e) {
            throw new TechnicalException(e);
        }
    }

    protected IDTokenValidator createHMACTokenValidator(final OidcConfiguration configuration, final JWSAlgorithm jwsAlgorithm,
                                                        final ClientID clientID, final Secret secret) {
        return new IDTokenValidator(configuration.findProviderMetadata().getIssuer(), clientID, jwsAlgorithm, secret);
    }

    public IDTokenClaimsSet validate(final JWT idToken, final Nonce expectedNonce)
        throws BadJOSEException, JOSEException {

        BadJOSEException badJOSEException = null;
        JOSEException joseException = null;
        for (final var idTokenValidator : idTokenValidators) {
            try {
                return idTokenValidator.validate(idToken, expectedNonce);
            } catch (final BadJOSEException e1) {
                logger.debug(e1.getMessage(), e1);
                badJOSEException = e1;
            } catch (final JOSEException e2) {
                logger.debug(e2.getMessage(), e2);
                joseException = e2;
            }
        }

        if (badJOSEException != null) {
            throw badJOSEException;
        } else if (joseException != null) {
            throw joseException;
        } else {
            throw new TechnicalException("Unable to validate the ID token");
        }
    }
}

Output:
【漏洞类型】ID Token 签名验证绕过漏洞
【严重性】High
【漏洞摘要】在 `OidcConfiguration` 类的 `internalInit` 方法中，ID Token 的签名验证默认情况下是强制的，但未明确禁止使用 "none" 算法（即无签名验证）。这意味着攻击者可能会通过使用无签名的 ID Token 来绕过身份验证机制。
【关键代码模式】
- `OidcConfiguration.java`
  - `internalInit` 方法中未明确禁止使用 "none" 算法
  - `findPkceMethod` 方法中对 PKCE 的支持进行检查
  - `TokenValidator.java`
    - `createRSATokenValidator` 方法用于创建 RSA 算法的 ID Token 验证器
    - `createHMACTokenValidator` 方法用于创建 HMAC 算法的 ID Token 验证器
【修复方法说明】在 `OidcConfiguration` 类中添加 `setAllowUnsignedIdTokens` 方法，并确保在初始化时禁止使用 "none" 算法，除非明确允许。同时，更新 `UserInfoOidcAuthenticator` 和 `TokenValidator` 类以使用新的配置属性。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
web/src/main/java/org/openmrs/web/filter/StartupFilter.java
### 代码内容：
package org.openmrs.web.filter;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.log.CommonsLogLogChute;
import org.apache.velocity.tools.Scope;
import org.apache.velocity.tools.ToolContext;
import org.apache.velocity.tools.ToolManager;
import org.apache.velocity.tools.config.DefaultKey;
import org.apache.velocity.tools.config.FactoryConfiguration;
import org.apache.velocity.tools.config.ToolConfiguration;
import org.apache.velocity.tools.config.ToolboxConfiguration;
import org.codehaus.jackson.map.ObjectMapper;
import org.openmrs.OpenmrsCharacterEscapes;
import org.openmrs.api.APIException;
import org.openmrs.api.context.Context;
import org.openmrs.util.*;
import org.openmrs.web.WebConstants;
import org.openmrs.web.filter.initialization.InitializationFilter;
import org.openmrs.web.filter.update.UpdateFilter;
import org.openmrs.web.filter.util.FilterUtil;
import org.openmrs.web.filter.util.LocalizationTool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Abstract class used when a small wizard is needed before Spring, jsp, etc has been started up.
 *
 * @see UpdateFilter
 * @see InitializationFilter
 */
public abstract class StartupFilter implements Filter {

	private static final Logger log = LoggerFactory.getLogger(StartupFilter.class);

	protected static VelocityEngine velocityEngine = null;

	public static final String AUTO_RUN_OPENMRS = "auto_run_openmrs";

	/**
	 * Set by the {@link #init(FilterConfig)} method so that we have access to the current
	 * {@link ServletContext}
	 */
	protected FilterConfig filterConfig = null;

	/**
	 * Records errors that will be displayed to the user
	 */
	protected Map<String, Object[]> errors = new HashMap<>();

	/**
	 * Messages that will be displayed to the user
	 */
	protected Map<String, Object[]> msgs = new HashMap<>();

	/**
	 * Used for configuring tools within velocity toolbox
	 */
	private ToolContext toolContext = null;

	/**
	 * The web.xml file sets this {@link StartupFilter} to be the first filter for all requests.
	 *
	 * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse,
	 *      javax.servlet.FilterChain)
	 */
	@Override
	public final void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
	        throws IOException, ServletException {
		if (skipFilter((HttpServletRequest) request)) {
			chain.doFilter(request, response);
		} else {

			HttpServletRequest httpRequest = (HttpServletRequest) request;
			HttpServletResponse httpResponse = (HttpServletResponse) response;

			String servletPath = httpRequest.getServletPath();
			// for all /images and /initfilter/scripts files, write the path
			// (the "/initfilter" part is needed so that the openmrs_static_context-servlet.xml file doesn't
			//  get instantiated early, before the locale messages are all set up)
			if (servletPath.startsWith("/images") || servletPath.startsWith("/initfilter/scripts")) {
				servletPath = servletPath.replaceFirst("/initfilter", "/WEB-INF/view"); // strip out the /initfilter part
				// writes the actual image file path to the response
				File file = new File(filterConfig.getServletContext().getRealPath(servletPath));
				if (httpRequest.getPathInfo() != null) {
					file = new File(file, httpRequest.getPathInfo());
				}

				InputStream imageFileInputStream = null;
				try {
					imageFileInputStream = new FileInputStream(file);
					OpenmrsUtil.copyFile(imageFileInputStream, httpResponse.getOutputStream());
				}
				catch (FileNotFoundException e) {
					log.error("Unable to find file: " + file.getAbsolutePath());
				}
				finally {
					if (imageFileInputStream != null) {
						try {
							imageFileInputStream.close();
						}
						catch (IOException io) {
							log.warn("Couldn't close imageFileInputStream: " + io);
						}
					}
				}
			} else if (servletPath.startsWith("/scripts")) {
				log.error(
				    "Calling /scripts during the initializationfilter pages will cause the openmrs_static_context-servlet.xml to initialize too early and cause errors after startup.  Use '/initfilter"
				            + servletPath + "' instead.");
			}
			// for anything but /initialsetup
			else if (!httpRequest.getServletPath().equals("/" + WebConstants.SETUP_PAGE_URL)
			        && !httpRequest.getServletPath().equals("/" + AUTO_RUN_OPENMRS)) {
				// send the user to the setup page
				httpResponse.sendRedirect("/" + WebConstants.WEBAPP_NAME + "/" + WebConstants.SETUP_PAGE_URL);
			} else {

				if ("GET".equals(httpRequest.getMethod())) {
					doGet(httpRequest, httpResponse);
				} else if ("POST".equals(httpRequest.getMethod())) {
					// only clear errors before POSTS so that redirects can show errors too.
					errors.clear();
					msgs.clear();
					doPost(httpRequest, httpResponse);
				}
			}
			// Don't continue down the filter chain otherwise Spring complains
			// that it hasn't been set up yet.
			// The jsp and servlet filter are also on this chain, so writing to
			// the response directly here is the only option
		}
	}

	/**
	 * Convenience method to set up the velocity context properly
	 */
	private void initializeVelocity() {
		if (velocityEngine == null) {
			velocityEngine = new VelocityEngine();

			Properties props = new Properties();
			props.setProperty(RuntimeConstants.RUNTIME_LOG, "startup_wizard_vel.log");
			// Linux requires setting logging properties to initialize Velocity Context.
			props.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS,
			    "org.apache.velocity.runtime.log.CommonsLogLogChute");
			props.setProperty(CommonsLogLogChute.LOGCHUTE_COMMONS_LOG_NAME, "initial_wizard_velocity");

			// so the vm pages can import the header/footer
			props.setProperty(RuntimeConstants.RESOURCE_LOADER, "class");
			props.setProperty("class.resource.loader.description", "Velocity Classpath Resource Loader");
			props.setProperty("class.resource.loader.class",
			    "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");

			try {
				velocityEngine.init(props);
			}
			catch (Exception e) {
				log.error("velocity init failed, because: " + e);
			}
		}
	}

	/**
	 * Called by {@link #doFilter(ServletRequest, ServletResponse, FilterChain)} on GET requests
	 *
	 * @param httpRequest
	 * @param httpResponse
	 */
	protected abstract void doGet(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
	        throws IOException, ServletException;

	/**
	 * Called by {@link #doFilter(ServletRequest, ServletResponse, FilterChain)} on POST requests
	 *
	 * @param httpRequest
	 * @param httpResponse
	 */
	protected abstract void doPost(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
	        throws IOException, ServletException;

	/**
	 * All private attributes on this class are returned to the template via the velocity context and
	 * reflection
	 *
	 * @param templateName the name of the velocity file to render. This name is prepended with
	 *            {@link #getTemplatePrefix()}
	 * @param referenceMap
	 * @param httpResponse
	 */
	protected void renderTemplate(String templateName, Map<String, Object> referenceMap, HttpServletResponse httpResponse)
	        throws IOException {
		// first we should get velocity tools context for current client request (within
		// his http session) and merge that tools context with basic velocity context
		if (referenceMap == null) {
			return;
		}

		Object locale = referenceMap.get(FilterUtil.LOCALE_ATTRIBUTE);
		ToolContext velocityToolContext = getToolContext(
		    locale != null ? locale.toString() : Context.getLocale().toString());
		VelocityContext velocityContext = new VelocityContext(velocityToolContext);

		for (Map.Entry<String, Object> entry : referenceMap.entrySet()) {
			velocityContext.put(entry.getKey(), entry.getValue());
		}

		Object model = getUpdateFilterModel();

		// put each of the private varibles into the template for convenience
		for (Field field : model.getClass().getDeclaredFields()) {
			try {
				field.setAccessible(true);
				velocityContext.put(field.getName(), field.get(model));
			}
			catch (IllegalArgumentException | IllegalAccessException e) {
				log.error("Error generated while getting field value: " + field.getName(), e);
			}
		}

		String fullTemplatePath = getTemplatePrefix() + templateName;
		InputStream templateInputStream = getClass().getClassLoader().getResourceAsStream(fullTemplatePath);
		if (templateInputStream == null) {
			throw new IOException("Unable to find " + fullTemplatePath);
		}

		velocityContext.put("errors", errors);
		velocityContext.put("msgs", msgs);

		// explicitly set the content type for the response because some servlet containers are assuming text/plain
		httpResponse.setContentType("text/html");

		try {
			velocityEngine.evaluate(velocityContext, httpResponse.getWriter(), this.getClass().getName(),
			    new InputStreamReader(templateInputStream, StandardCharsets.UTF_8));
		}
		catch (Exception e) {
			throw new APIException("Unable to process template: " + fullTemplatePath, e);
		}
	}

	/**
	 * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
	 */
	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		this.filterConfig = filterConfig;
		initializeVelocity();
	}

	/**
	 * @see javax.servlet.Filter#destroy()
	 */
	@Override
	public void destroy() {
	}

	/**
	 * This string is prepended to all templateNames passed to
	 * {@link #renderTemplate(String, Map, HttpServletResponse)}
	 *
	 * @return string to prepend as the path for the templates
	 */
	protected String getTemplatePrefix() {
		return "org/openmrs/web/filter/";
	}

	/**
	 * The model that is used as the backer for all pages in this startup wizard. Should never return
	 * null.
	 *
	 * @return the stored formbacking/model object
	 */
	protected abstract Object getUpdateFilterModel();

	/**
	 * If this returns true, this filter fails early and quickly. All logic is skipped and startup and
	 * usage continue normally.
	 *
	 * @return true if this filter can be skipped
	 */
	public abstract boolean skipFilter(HttpServletRequest request);

	/**
	 * Convenience method to read the last 5 log lines from the MemoryAppender
	 *
	 * The log lines will be added to the "logLines" key
	 *
	 * @param result A map to be returned as a JSON document
	 */
	protected void addLogLinesToResponse(Map<String, Object> result) {
		MemoryAppender appender = OpenmrsUtil.getMemoryAppender();
		if (appender != null) {
			List<String> logLines = appender.getLogLines();

			// truncate the list to the last five so we don't overwhelm jquery
			if (logLines.size() > 5) {
				logLines = logLines.subList(logLines.size() - 5, logLines.size());
			}

			result.put("logLines", logLines);
		} else {
			result.put("logLines", Collections.emptyList());
		}
	}

	/**
	 * Convenience method to convert the given object to a JSON string. Supports Maps, Lists, Strings,
	 * Boolean, Double
	 *
	 * @param object object to convert to json
	 * @return JSON string to be eval'd in javascript
	 */
	protected String toJSONString(Object object) {
		ObjectMapper mapper = new ObjectMapper();
		mapper.getJsonFactory().setCharacterEscapes(new OpenmrsCharacterEscapes());
		try {
			return mapper.writeValueAsString(object);
		}
		catch (IOException e) {
			log.error("Failed to convert object to JSON");
			throw new APIException(e);
		}
	}

	/**
	 * Gets tool context for specified locale parameter. If context does not exists, it creates new
	 * context, configured for that locale. Otherwise, it changes locale property of
	 * {@link LocalizationTool} object, that is being contained in tools context
	 *
	 * @param locale the string with locale parameter for configuring tools context
	 * @return the tool context object
	 */
	public ToolContext getToolContext(String locale) {
		Locale systemLocale = LocaleUtility.fromSpecification(locale);
		//Defaults to en if systemLocale is null or invalid e.g en_GBs
		if (systemLocale == null || !ArrayUtils.contains(Locale.getAvailableLocales(), systemLocale)) {
			systemLocale = Locale.ENGLISH;
		}
		// If tool context has not been configured yet
		if (toolContext == null) {
			// first we are creating manager for tools, factory for configuring tools
			// and empty configuration object for velocity tool box
			ToolManager velocityToolManager = new ToolManager();
			FactoryConfiguration factoryConfig = new FactoryConfiguration();
			// since we are using one tool box for all request within wizard
			// we should propagate toolbox's scope on all application
			ToolboxConfiguration toolbox = new ToolboxConfiguration();
			toolbox.setScope(Scope.APPLICATION);
			// next we are directly configuring custom localization tool by
			// setting its class name, locale property etc.
			ToolConfiguration localizationTool = new ToolConfiguration();
			localizationTool.setClassname(LocalizationTool.class.getName());
			localizationTool.setProperty(ToolContext.LOCALE_KEY, systemLocale);
			localizationTool.setProperty(LocalizationTool.BUNDLES_KEY, "messages");
			// and finally we are adding just configured tool into toolbox
			// and creating tool context for this toolbox
			toolbox.addTool(localizationTool);
			factoryConfig.addToolbox(toolbox);
			velocityToolManager.configure(factoryConfig);
			toolContext = velocityToolManager.createContext();
			toolContext.setUserCanOverwriteTools(true);
		} else {
			// if it already has been configured, we just pull out our custom localization tool
			// from tool context, then changing its locale property and putting this tool back to the context
			// First, we need to obtain the value of default key annotation of our localization tool
			// class using reflection
			Annotation annotation = LocalizationTool.class.getAnnotation(DefaultKey.class);
			DefaultKey defaultKeyAnnotation = (DefaultKey) annotation;
			String key = defaultKeyAnnotation.value();
			//
			LocalizationTool localizationTool = (LocalizationTool) toolContext.get(key);
			localizationTool.setLocale(systemLocale);
			toolContext.put(key, localizationTool);
		}
		return toolContext;
	}
}


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/registerinline.vm
### 代码内容：
## ---------------------------------------------------------------------------
## See the NOTICE file distributed with this work for additional
## information regarding copyright ownership.
##
## This is free software; you can redistribute it and/or modify it
## under the terms of the GNU Lesser General Public License as
## published by the Free Software Foundation; either version 2.1 of
## the License, or (at your option) any later version.
##
## This software is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
## Lesser General Public License for more details.
##
## You should have received a copy of the GNU Lesser General Public
## License along with this software; if not, write to the Free
## Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
## 02110-1301 USA, or see the FSF site: http://www.fsf.org.
## ---------------------------------------------------------------------------
#if (!$xwiki.hasAccessLevel('view', 'XWiki.Registration') || $xwiki.getDocument('XWiki.Registration').isNew())
  ## Display the static content included in this template, as there's no override in the wiki.

$services.localization.render('core.register.title')

  #if (!$reg || $reg < 0)

$services.localization.render('core.register.welcome')

  #end
  #if ($reg && $reg <= 0)
    #if ($reg == -2)
      #error($services.localization.render('core.register.passwordMismatch'))
    #elseif ($reg == -3)
      #error($services.localization.render('core.register.userAlreadyExists'))
    #elseif ($reg == -4)
      #error($services.localization.render('core.register.invalidUsername'))
    #elseif ($reg == -8)
      #error($services.localization.render('core.register.userAlreadyExists'))
    #else
      #error($services.localization.render('core.register.registerFailed', [$reg]))
    #end
  #elseif ($reg)
    #set ($xwname = "XWiki.${request.xwikiname}")
    #info($services.localization.render('core.register.successful', [$xwiki.getUserName($xwname),
      $escapetool.xml($request.xwikiname)]))
  #end
  #if (!$reg || $reg < 0)
    <form id="register" action="" method="post" class="xform third">

        <input type="hidden" name="form_token" value="$!services.csrf.token" />
        <input type="hidden" name="register" value="1" />
        <input type="hidden" name="xredirect" value="$!request.xredirect" />
        #set ($class = $xwiki.getClass('XWiki.XWikiUsers'))
        #set ($obj = $class.newObject())
        #set ($serverobj = $class.newObject())
        #set ($discard = $doc.use('XWiki.XWikiUsers'))
        #if ($request.register_first_name)
          $doc.set('first_name', $request.register_first_name)
        #end
        #if ($request.register_last_name)
          $doc.set('last_name', $request.register_last_name)
        #end

          #set ($prop = $class.first_name)

$services.localization.render('core.register.firstName')

$doc.displayEdit($prop, 'register_',  $obj)

          #set ($prop = $class.last_name)

$services.localization.render('core.register.lastName')

$doc.displayEdit($prop, 'register_',  $obj)

              $services.localization.render('core.register.username')
              $services.localization.render('core.validation.required')

            <input name="xwikiname" id="register_username" type="text" size="20"
              onfocus="prepareName(document.forms.register);" />

          #set ($prop = $class.password)

              $services.localization.render('core.register.password')
              $services.localization.render('core.validation.required')

$doc.displayEdit($prop, 'register_',  $obj).replace('type=', 'autocomplete="off" type=')

              $services.localization.render('core.register.passwordRepeat')
              $services.localization.render('core.validation.required')

$doc.displayEdit($prop, 'register2_',  $obj).replace('type=', 'autocomplete="off" type=')

          #set ($prop = $class.email)

$services.localization.render('core.register.email')

$doc.displayEdit($prop, 'register_',  $obj)

          <input type="submit" value="$services.localization.render('core.register.submit')" class="button"/>

    </form>
  #end
#else
  ## An override exists in the wiki, display it.
  #set ($doc = $xwiki.getDocument('XWiki.Registration'))
  $xwiki.includeTopic('XWiki.Registration', false)
#end

Output:
【漏洞类型】XSS（跨站脚本攻击）
【严重性】Medium
【漏洞摘要】原始代码中，用户输入的`xredirect`参数在`<input type="hidden" name="xredirect" value="$!request.xredirect" />`这行代码中直接输出，未进行适当的转义或验证，导致可能存在XSS攻击风险。攻击者可以利用此漏洞注入恶意脚本，使得在用户注册后重定向到恶意页面。
【关键代码模式】用户输入直接输出到HTML或JavaScript，未进行转义或验证。关键词包括`$request.xredirect`和`<input type="hidden" name="xredirect" value="$!request.xredirect" />`。
【修复方法说明】修复方法通过在`$!request.xredirect`的值上应用`$escapetool.xml`转义函数，防止了恶意脚本的注入，从而解决了XSS漏洞。最佳实践是始终对用户输入进行适当的转义和验证，以防止跨站脚本攻击。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/main/java/org/xwiki/skinx/internal/AbstractSxExportURLFactoryActionHandler.java
### 代码内容：
/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.skinx.internal;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.security.authorization.ContextualAuthorizationManager;
import org.xwiki.security.authorization.Right;
import org.xwiki.url.filesystem.FilesystemExportContext;

import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.internal.model.LegacySpaceResolver;
import com.xpn.xwiki.web.ExportURLFactoryActionHandler;
import com.xpn.xwiki.web.sx.Extension;
import com.xpn.xwiki.web.sx.SxDocumentSource;
import com.xpn.xwiki.web.sx.SxResourceSource;
import com.xpn.xwiki.web.sx.SxSource;

/**
 * Handles SX URL rewriting, by extracting and rendering the SX content in a file on disk and generating a URL
 * pointing to it.
 *
 * @version $Id$
 * @since 6.2RC1
 */
public abstract class AbstractSxExportURLFactoryActionHandler implements ExportURLFactoryActionHandler
{
    /** If the user passes this parameter in the URL, we will look for the script in the jar files. */
    private static final String JAR_RESOURCE_REQUEST_PARAMETER = "resource";

    private static final char URL_PATH_SEPARATOR = '/';

    @Inject
    private LegacySpaceResolver legacySpaceResolve;

    @Inject
    private ContextualAuthorizationManager authorizationManager;

    protected abstract String getSxPrefix();

    protected abstract String getFileSuffix();

    /**
     * Get the type of extension, depends on the type of action.
     *
     * @return a new object which extends Extension.
     */
    public abstract Extension getExtensionType();

    @Override
    public URL createURL(String spaces, String name, String queryString, String anchor, String wikiId,
        XWikiContext context, FilesystemExportContext exportContext) throws Exception
    {
        // Check if the current user has the right to view the SX file. We do this since this is what would happen
        // in XE when a SX action is called (check done in XWikiAction).
        // Note that we cannot just open an HTTP connection to the SX action here since we wouldn't be authenticated...
        // Thus we have to simulate the same behavior as the SX action...

        List<String> spaceNames = this.legacySpaceResolve.resolve(spaces);
        DocumentReference sxDocumentReference = new DocumentReference(wikiId, spaceNames, name);
        this.authorizationManager.checkAccess(Right.VIEW, sxDocumentReference);

        // Set the SX document as the current document in the XWiki Context since unfortunately the SxSource code
        // uses the current document in the context instead of accepting it as a parameter...
        XWikiDocument sxDocument = context.getWiki().getDocument(sxDocumentReference, context);

        Map<String, Object> backup = new HashMap<>();
        XWikiDocument.backupContext(backup, context);
        try {
            sxDocument.setAsContextDoc(context);
            return processSx(spaceNames, name, queryString, context, exportContext);
        } finally {
            XWikiDocument.restoreContext(backup, context);
        }
    }

    private URL processSx(List<String> spaceNames, String name, String queryString, XWikiContext context,
        FilesystemExportContext exportContext) throws Exception
    {
        SxSource sxSource = null;

        // Check if we have the JAR_RESOURCE_REQUEST_PARAMETER parameter in the query string
        List<NameValuePair> params = URLEncodedUtils.parse(queryString, StandardCharsets.UTF_8);
        for (NameValuePair param : params) {
            if (param.getName().equals(JAR_RESOURCE_REQUEST_PARAMETER)) {
                sxSource = new SxResourceSource(param.getValue());
                break;
            }
        }

        if (sxSource == null) {
            sxSource = new SxDocumentSource(context, getExtensionType());
        }

        String content = getContent(sxSource, exportContext);

        // Write the content to file
        // We need a unique name for that SSX content
        String targetPath = String.format("%s/%s/%s", getSxPrefix(), StringUtils.join(spaceNames, '/'), name);
        File targetDirectory = new File(exportContext.getExportDir(), targetPath);
        if (!targetDirectory.exists()) {
            targetDirectory.mkdirs();
        }
        File targetLocation = File.createTempFile(getSxPrefix(), "." + getFileSuffix(), targetDirectory);
        FileUtils.writeStringToFile(targetLocation, content);

        // Rewrite the URL
        StringBuilder path = new StringBuilder("file://");

        // Adjust based on current document's location. We need to account for the fact that the current document
        // is stored in subdirectories inside the top level "pages" directory. Since the SX files are also in top
        // subdirectories, we need to compute the path to them.
        path.append(StringUtils.repeat("../", exportContext.getDocParentLevel()));

        path.append(getSxPrefix());
        path.append(URL_PATH_SEPARATOR);
        for (String spaceName : spaceNames) {
            path.append(encodeURLPart(spaceName));
            path.append(URL_PATH_SEPARATOR);
        }
        path.append(encodeURLPart(name));
        path.append(URL_PATH_SEPARATOR);
        path.append(encodeURLPart(targetLocation.getName()));

        return new URL(path.toString());
    }

    protected String getContent(SxSource sxSource, FilesystemExportContext exportContext)
    {
        String content;

        // We know we're inside a SX file located at "<S|J>sx/<Space>/<Page>/<s|j>sx<NNN>.<css|js>". Inside this CSS
        // there can be URLs and we need to ensure that the prefix for these URLs lead to the root of the path, i.e.
        // 3 levels up ("../../../").
        // To make this happen we reuse the Doc Parent Level from FileSystemExportContext to a fixed value of 3.
        // We also make sure to put back the original value
        int originalDocParentLevel = exportContext.getDocParentLevel();
        try {
            exportContext.setDocParentLevels(3);
            content = sxSource.getContent();
        } finally {
            exportContext.setDocParentLevels(originalDocParentLevel);
        }

        return content;
    }

    private String encodeURLPart(String part) throws IOException
    {
        return URLEncoder.encode(part, "UTF-8");
    }
}

Output:
【漏洞类型】路径遍历漏洞
【严重性】High
【漏洞摘要】这是一个路径遍历漏洞。原始代码在处理`sxDocumentReference`时，直接使用用户输入的空间名称和页面名称来构建文件路径（第104行）。如果用户在URL中插入了`../`或其他路径遍历攻击字符串，可以覆盖原本的安全路径，从而将文件写入任意位置。此外，`processSx`方法中的路径构建也未对`spaceNames`和`name`进行验证（第132行），这进一步增加了漏洞的风险。
【关键代码模式】
- `resolve(spaces)`：解析用户输入的空间名称。
- `new DocumentReference(wikiId, spaceNames, name)`：根据用户输入创建文档引用。
- `StringUtils.join(spaceNames, '/')`：将空间名称连接成路径。
- `File.createTempFile(getSxPrefix(), "." + getFileSuffix(), targetDirectory)`：在目标目录下创建临时文件。
- `path.append(StringUtils.repeat("../", exportContext.getDocParentLevel()))`：根据上下文调整文件路径。
【修复方法说明】修复方法通过使用文档的ID（第100行和第128行）而不是空间名称和页面名称来构建文件路径，避免了路径遍历的可能性。此外，将`Doc Parent Level`从3改为2，以正确调整文件路径（第157行），确保文件写入的安全性和完整性。