29 Data and Code Data Is Data and Code Is Code

29 Data and Code Data Is Data and Code Is Code #

Today, I would like to talk to you about the issue of data and code.

As the title of this lecture, “Data is Data, Code is Code,” suggests, many vulnerabilities in web security stem from treating data as code, which leads to injection-type problems, such as:

When the client provides a query value to the server, it is a piece of data that becomes part of an SQL query. Hackers manipulate this value to inject SQL code and run it on the server, effectively turning the data of the query conditions into query code. This type of attack is called SQL injection.

In the case of rule engines, we may use dynamic languages to perform calculations. Similar to SQL injection, any externally input data must be treated as data. However, if hackers exploit this to pass in code, the code may be dynamically executed. This type of attack is called code injection.

For functions like user registration and commenting, the server collects information from the client. Originally, information such as usernames and email addresses is pure text. However, hackers can replace this information with JavaScript code. As a result, when this information is rendered on the page, it may be executed as JavaScript code. In some cases, the server may even store such code as ordinary information in the database. Hackers exploit this by constructing JavaScript code to modify page rendering, steal information, and even carry out worm attacks. This type of attack is known as cross-site scripting (XSS) attack.

Today, we will examine these three problems through case studies and explore ways to address them.

SQL Injection can do more than you think #

We have all heard of SQL injection and may know the most classic example of SQL injection, which is logging in by constructing ‘or'1’=‘1 as the password. This simple attack method used to be able to bypass many backend logins over a decade ago, but now it is difficult to be effective.

In recent years, our security awareness has increased, and we all know that using parameterized queries can avoid SQL injection issues. The principle is that if we use parameterized queries, the parameters can only be treated as normal data and cannot be part of the SQL statement itself, thus effectively avoiding SQL injection issues.

Although we have started to pay attention to SQL injection issues, there are still some misconceptions, mainly manifested in the following three aspects:

First, thinking that SQL injection issues can only occur in Http GET requests, that is, injection points can only be generated from parameters passed through the URL. This is a dangerous idea. In terms of the difficulty of injection, there is no difference between modifying the QueryString in the URL and modifying the data in the Post request body, because hackers inject using tools, not by modifying the URL in the browser. Even cookies can be used for SQL injection, and any place that provides data can become an injection point.

Second, thinking that interfaces that do not return data cannot have injection problems. In fact, hackers can completely exploit incorrect SQL statements to cause execution errors. If the server directly displays error messages, the data that hackers need may be brought out, thereby achieving the purpose of querying data. Moreover, even without detailed error information, hackers can still attack through a technique called blind injection. I will explain this in more detail later.

Third, thinking that the scope of SQL injection is limited to bypassing login by using short-circuits, and only strengthening the defense of login operations is needed. First of all, SQL injection can completely achieve dumping the entire database, that is, downloading the contents of the entire database (we will demonstrate this later). The harm of SQL injection is not only about bypassing backend logins. Secondly, according to the “weakest link” principle, the security of the entire site is limited by the security level of the weakest area. Therefore, for security issues, all modules of the site must be treated equally, and it is not just a matter of strengthening the defense of so-called key modules.

In daily development, although we use frameworks for data access, there may still be injection problems due to negligence. Next, I will use an actual example with the professional SQL injection tool sqlmap to test SQL injection.

First, when the program starts, create a userdata table (the table only has three columns: ID, username, and password) using JdbcTemplate and initialize two user information. Then, create an Http POST interface that does not return any data. In the implementation, we use SQL concatenation to concatenate the incoming username parameter into the LIKE clause to implement fuzzy search.

// Table structure and data initialization at program startup
@PostConstruct
public void init() {

    // Drop table
    jdbcTemplate.execute("drop table IF EXISTS `userdata`;");

    // Create table, containing three columns: ID, username, and password
    jdbcTemplate.execute("create TABLE `userdata` (\n" +
            "  `id` bigint(20) NOT NULL AUTO_INCREMENT,\n" +
            "  `name` varchar(255) NOT NULL,\n" +
            "  `password` varchar(255) NOT NULL,\n" +
            "  PRIMARY KEY (`id`)\n" +
            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");

    // Insert two test data
    jdbcTemplate.execute("INSERT INTO `userdata` (name,password) VALUES ('test1','haha1'),('test2','haha2')");
}

@Autowired
private JdbcTemplate jdbcTemplate;

// User fuzzy search interface
@PostMapping("jdbcwrong")
public void jdbcwrong(@RequestParam("name") String name) {

    // Use SQL concatenation to concatenate the name parameter into the LIKE clause
    log.info("{}", jdbcTemplate.queryForList("SELECT id,name FROM userdata WHERE name LIKE '%" + name + "%'"));

}

Use sqlmap to explore this interface:

python sqlmap.py -u  http://localhost:45678/sqlinject/jdbcwrong --data name=test

After a while, sqlmap gives the following result:

img

It can be seen that there are two possible injection methods for the name parameter of this interface: one is error-based injection, and the other is time-based blind injection.

Next, with just three simple steps, we can directly extract the entire content of the user table.

Step 1, query the current database:

python sqlmap.py -u  http://localhost:45678/sqlinject/jdbcwrong --data name=test --current-db

It shows that the current database is common_mistakes:

current database: 'common_mistakes'

Step 2, query the tables in the database:

python sqlmap.py -u  http://localhost:45678/sqlinject/jdbcwrong --data name=test --tables -D "common_mistakes"

It can be seen that there is a sensitive table userdata:

Database: common_mistakes

[7 tables]

+--------------------+

| user               |

| common_store       |

| hibernate_sequence |

| m                  |

| news               |

| r                  |

| userdata           |

+--------------------+

Step 3, query the data of userdata:

python sqlmap.py -u  http://localhost:45678/sqlinject/jdbcwrong --data name=test -D "common_mistakes" -T "userdata" --dump

You can see the user password information at a glance. Of course, you can also continue to view data from other tables:

Database: common_mistakes

Table: userdata

[2 entries]

+----+-------+----------+

| id | name  | password |

+----+-------+----------+

| 1  | test1 | haha1    |

| 2  | test2 | haha2    |

+----+-------+----------+

In the logs, you can see that sqlmap achieves database dumping by making the error information after executing SQL statements contain field contents. Pay attention to the second line of the error log, where the error message includes the value of the password field of the user with ID 2, which is “haha2”. This is the basic principle of error-based injection. [13:22:27.375] [http-nio-45678-exec-10] [ERROR] [o.a.c.c.C.[.[.[/].[dispatcherServlet]:175 ] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DuplicateKeyException: StatementCallback; SQL [SELECT id,name FROM userdata WHERE name LIKE ‘%test’||(SELECT 0x694a6e64 WHERE 3941=3941 AND (SELECT 9927 FROM(SELECT COUNT(*),CONCAT(0x71626a7a71,(SELECT MID((IFNULL(CAST(password AS NCHAR),0x20)),1,54) FROM common_mistakes.userdata ORDER BY id LIMIT 1,1),0x7170706271,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a))||’%’]; Duplicate entry ‘qbjzqhaha2qppbq1’ for key ‘<group_key>’; nested exception is java.sql.SQLIntegrityConstraintViolationException: Duplicate entry ‘qbjzqhaha2qppbq1’ for key ‘<group_key>’] with root cause

java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'qbjzqhaha2qppbq1' for key '<group_key>'

Let’s implement an ExceptionHandler to handle the exception and see if we can solve the injection problem:

@ExceptionHandler
public void handle(HttpServletRequest req, HandlerMethod method, Exception ex) {
    log.warn(String.format("Exception occurred while accessing %s -> %s!", req.getRequestURI(), method.toString()), ex);
}

After restarting the program and running the previous sqlmap command again, you can see that the error-based injection is no longer possible, but time-based blind injection can still be used to query the data table:

img

Blind injection refers to the injection where no execution result (even an error message) can be obtained from the server, and only different states of true and false conditions in the SQL can be inferred. For example, in boolean blind injection, a “true” condition might return a 200 status code, while a “false” condition might return a 500 error status code; or a “true” condition might result in content output, while a “false” condition might not result in any output. In summary, any different output that can be obtained for different SQL injections is acceptable.

In this case, since the interface does not have any output and errors are completely suppressed, boolean blind injection is not applicable. Therefore, the next best option is time-based blind injection. This means adding a SLEEP function to the true and false conditions to determine the result by judging the response time of the interface.

Regardless of the type of blind injection, it is all about achieving the goal through true and false conditions. You may wonder how data extraction can be achieved through true and false conditions?

Think about it, we may not be able to directly query the value of the password field, but we can query each individual character and determine if the first character is ‘a’, ‘b’, etc. When we find that the response slows down when querying ‘h’, we naturally know that it is true and that the first character is ‘h’. This way, the entire value can be queried character by character.

Therefore, when sqlmap returns the data, it also displays the results character by character, and the entire process of time-based blind injection is much slower compared to error-based injection.

You can introduce the p6spy tool to print out all the executed SQL statements and analyze the principles behind the SQL statements constructed by sqlmap:

<dependency>
    <groupId>com.github.gavlyukovskiy</groupId>
    <artifactId>p6spy-spring-boot-starter</artifactId>
    <version>1.6.1</version>
</dependency>

img

Therefore, even if error messages and error codes are suppressed, it is still not possible to completely prevent SQL injection. The real solution is to use parameterized queries, where any external input values are treated as data.

For example, for the previous interface, you can use the ‘?’ character as a parameter placeholder in the SQL statement and provide the parameter value. With this modification, sqlmap is no longer effective:

@PostMapping("jdbcright")
public void jdbcright(@RequestParam("name") String name) {
    log.info("{}", jdbcTemplate.queryForList("SELECT id,name FROM userdata WHERE name LIKE ?", "%" + name + "%"));
}

Similarly, in MyBatis, you also need to use parameterized SQL statements. In MyBatis, ‘#{}’ is a parameterized placeholder, while ‘${}’ is just a placeholder substitution.

For example, for the LIKE statement, you need to use ‘#{}’ as it adds single quotes to the parameter, causing a syntax error for the LIKE operator. Some developers may resort to using ‘${}’ as an alternative, for example:

@Select("SELECT id,name FROM `userdata` WHERE name LIKE '%${name}%'")
List<UserData> findByNameWrong(@Param("name") String name);

However, this method directly substitutes the external input into the IN clause, causing an injection vulnerability:

@PostMapping("mybatiswrong2")
public List mybatiswrong2(@RequestParam("names") String names) {
    return userDataMapper.findByNamesWrong(names);
}

You can test this using the following command:

python sqlmap.py -u  http://localhost:45678/sqlinject/mybatiswrong2 --data names="'test1','test2'"

As a result, four injection methods are available, including boolean blind injection, error-based injection, time-based blind injection, and union-based query injection:

img

The correct approach is to pass a List to MyBatis and use the foreach tag to concatenate the contents in the IN clause, ensuring that each item in the IN clause is injected as a parameter using ‘#{}’:

@PostMapping("mybatisright2")
public List mybatisright2(@RequestParam("names") List<String> names) {
    return userDataMapper.findByNamesRight(names);
}
<select id="findByNamesRight" resultType="org.geekbang.time.commonmistakes.codeanddata.sqlinject.UserData">
    SELECT id,name FROM `userdata` WHERE name in
    <foreach collection="names" item="item" open="(" separator="," close=")">
        #{item}
    </foreach>
</select>

After making this modification, this interface is no longer vulnerable to injection. You can test it yourself.

Beware of Code Injection Vulnerabilities in Dynamic Code Execution #

In summary, the reason for the SQL injection vulnerability we just saw is that the hacker injected SQL attack code into the SQL statement through the parameter. Similarly, for any other interpreted language code that is executed, it can also have similar injection vulnerabilities. Let’s look at a case where executing dynamic JavaScript code leads to an injection vulnerability.

Now, we want to dynamically validate username rules: obtain a JavaScript script engine through ScriptEngineManager and use Java code to dynamically execute JavaScript code. When the external username is admin, it returns 1; otherwise, it returns 0:

private ScriptEngineManager scriptEngineManager = new ScriptEngineManager();

// Obtain a JavaScript script engine

private ScriptEngine jsEngine = scriptEngineManager.getEngineByName("js");

@GetMapping("wrong")
public Object wrong(@RequestParam("name") String name) {
    try {
        // Dynamically execute JavaScript script using eval. Here, the name parameter is mixed into the JavaScript code using string concatenation
        return jsEngine.eval(String.format("var name='%s'; name=='admin'?1:0;", name));
    } catch (ScriptException e) {
        e.printStackTrace();
    }
    return null;
}

There is no problem with this function itself:

img

However, if we modify the passed-in username to:

haha';java.lang.System.exit(0);'

We can achieve the goal of closing the entire program. The reason is that we directly concatenate the code and data together. If the external constructs a special username that closes the single quotation mark of the string and then execute a System.exit command, the script won’t throw an error and the command will be executed.

There are two ways to solve this problem.

The first method is similar to solving SQL injection, where the externally passed condition data should be treated as data only. We can bind and initialize the name variable by using SimpleBindings, instead of directly concatenating the JavaScript code:

@GetMapping("right")
public Object right(@RequestParam("name") String name) {
    try {
        // External parameter
        Map<String, Object> parm = new HashMap<>();
        parm.put("name", name);
        // The name parameter is bound and passed to the eval method instead of concatenating JavaScript code
        return jsEngine.eval("name=='admin'?1:0;", new SimpleBindings(parm));
    } catch (ScriptException e) {
        e.printStackTrace();
    }
    return null;
}

This avoids the injection problem:

img

The second method is to use SecurityManager in conjunction with AccessControlContext to build a sandbox environment for script execution. The permission for all operations that scripts can execute is precisely set through the setPermissions method:

@Slf4j
public class ScriptingSandbox {
    private ScriptEngine scriptEngine;
    private AccessControlContext accessControlContext;
    private SecurityManager securityManager;
    private static ThreadLocal<Boolean> needCheck = ThreadLocal.withInitial(() -> false);

    public ScriptingSandbox(ScriptEngine scriptEngine) throws InstantiationException {
        this.scriptEngine = scriptEngine;
        securityManager = new SecurityManager() {
            // Only checks permissions when needed
            @Override
            public void checkPermission(Permission perm) {
                if (needCheck.get() && accessControlContext != null) {
                    super.checkPermission(perm, accessControlContext);
                }
            }
        };
        // Set the required permissions for script execution
        setPermissions(Arrays.asList(
                new RuntimePermission("getProtectionDomain"),
                new PropertyPermission("jdk.internal.lambda.dumpProxyClasses","read"),
                new FilePermission(Shell.class.getProtectionDomain().getPermissions().elements().nextElement().getName(),"read"),
                new RuntimePermission("createClassLoader"),
                new RuntimePermission("accessClassInPackage.jdk.internal.org.objectweb.*"),
                new RuntimePermission("accessClassInPackage.jdk.nashorn.internal.*"),
                new RuntimePermission("accessDeclaredMembers"),
                new ReflectPermission("suppressAccessChecks")
        ));
    }

    // Set permissions for the execution context 
    public void setPermissions(List<Permission> permissionCollection) {
        Permissions perms = new Permissions();
        if (permissionCollection != null) {
            for (Permission p : permissionCollection) {
                perms.add(p);
            }
        }
        ProtectionDomain domain = new ProtectionDomain(new CodeSource(null, (CodeSigner[]) null), perms);
        accessControlContext = new AccessControlContext(new ProtectionDomain[]{domain});
    }

    public Object eval(final String code) {
        SecurityManager oldSecurityManager = System.getSecurityManager();
        System.setSecurityManager(securityManager);
        needCheck.set(true);
        try {
            // Execute the script under the protection of AccessController
            return AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
                try {
                    return scriptEngine.eval(code);
                } catch (ScriptException e) {
                    e.printStackTrace();
                }
                return null;
            }, accessControlContext);
        } catch (Exception ex) {
            log.error("Sorry, cannot execute the script {}", code, ex);
        } finally {
            needCheck.set(false);
            System.setSecurityManager(oldSecurityManager);
        }
        return null;
    }
}

Write a test code and use ScriptingSandbox sandbox utility class defined just now to execute the script:

@GetMapping("right2")
public Object right2(@RequestParam("name") String name) throws InstantiationException {
    // Execute the script using the sandbox
    ScriptingSandbox scriptingSandbox = new ScriptingSandbox(jsEngine);
    return scriptingSandbox.eval(String.format("var name='%s'; name=='admin'?1:0;", name));
}

This time, let’s use the previous injection script to call this interface:

http://localhost:45678/codeinject/right2?name=haha%27;java.lang.System.exit(0);%27

As you can see, an AccessControlException exception is thrown in the result, and the injection attack has failed:

[13:09:36.080] [http-nio-45678-exec-1] [ERROR] [o.g.t.c.c.codeinject.ScriptingSandbox:77  ] - Sorry, cannot execute the script var name='haha';java.lang.System.exit(0);''; name=='admin'?1:0;
java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "exitVM.0")
  at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
  at java.lang.SecurityManager.checkPermission(SecurityManager.java:585)
  at org.geekbang.time.commonmistakes.codeanddata.codeinject.ScriptingSandbox$1.checkPermission(ScriptingSandbox.java:30)
  at java.lang.SecurityManager.checkExit(SecurityManager.java:761)
  at java.lang.Runtime.exit(Runtime.java:107)

In actual applications, we can consider using both methods to ensure the security of code execution.

XSS Must Be Rigorously Prevented in All Areas #

For business development, XSS issues must also be taken seriously.

The root cause of XSS vulnerabilities lies in the fact that the original place where normal user input or data is expected to be entered has been replaced by JavaScript scripts by hackers. The page directly displays this data without escaping it, allowing the script to be executed. What’s even worse is that the script is saved to the database without any escaping process. When the page loads the data, the script embedded in the data is treated as code and executed. Hackers can exploit this vulnerability to steal sensitive data, or trick users into visiting malicious websites.

Let’s write some code to test this. First, the server defines two interfaces. The index interface queries user information and returns it to the XSS page, while the save interface uses the @RequestParam annotation to receive the username and save it into the database. Then, redirect the browser to the index interface:

@RequestMapping("xss")
@Slf4j
@Controller
public class XssController {

    @Autowired
    private UserRepository userRepository;

    // Display the XSS page
    @GetMapping
    public String index(ModelMap modelMap) {
        // Query the database
        User user = userRepository.findById(1L).orElse(new User());
        // Provide the model for the view
        modelMap.addAttribute("username", user.getName());
        return "xss";
    }

    // Save user information
    @PostMapping
    public String save(@RequestParam("username") String username, HttpServletRequest request) {
        User user = new User();
        user.setId(1L);
        user.setName(username);
        userRepository.save(user);
        // Redirect to the home page after saving
        return "redirect:/xss/";
    }

}

// User class serving as DTO and Entity
@Entity
@Data
public class User {

    @Id
    private Long id;
    private String name;

}

We use the Thymeleaf template engine to render the page. The template code is relatively simple. The username is displayed in a label when the page loads. When the user enters the username and submits the form, the save interface is called to create the user:

<div style="font-size: 14px">
    <form id="myForm" method="post" th:action="@{/xss/}">
        <label th:utext="${username}"/>
        <input id="username" name="username" size="100" type="text"/>
        <button th:text="Register" type="submit"/>
    </form>
</div>

After opening the XSS page, enter <script>alert('XSS')</script> in the text box and click the “Register” button. An alert dialog will pop up:

img

img

Moreover, the script is saved to the database:

img

You may think that the correct solution is to HTML-encode the data. Since we retrieve the request parameters through @RequestParam, we can implement a conversion logic in the @InitBinder method to perform HTML escaping on the strings:

@ControllerAdvice
public class SecurityAdvice {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        // Register a custom editor
        binder.registerCustomEditor(String.class, new PropertyEditorSupport() {
            @Override
            public String getAsText() {
                Object value = getValue();
                return value != null ? value.toString() : "";
            }
            @Override
            public void setAsText(String text) {
                // Perform HTML escaping when assigning values
                setValue(text == null ? null : HtmlUtils.htmlEscape(text));
            }
        });
    }

}

Indeed, this approach is feasible for this scenario. The data stored in the database is escaped, so it will be displayed as HTML on the page instead of being treated as a script:

img

img

However, this approach makes a serious mistake by not addressing security issues at the root. Because @InitBinder is a Spring Web-level processing logic, this approach will not work if there is code that directly retrieves data from the HTTP request instead of using @RequestParam. For example:

user.setName(request.getParameter("username"));

A more reasonable solution is to define a servlet filter and implement unified parameter substitution at the servlet level using HttpServletRequestWrapper:

// Custom filter
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class XssFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(new XssRequestWrapper((HttpServletRequest) request), response);
    }
}

public class XssRequestWrapper extends HttpServletRequestWrapper {
    public XssRequestWrapper(HttpServletRequest request) {
        super(request);
    }
    @Override
    public String[] getParameterValues(String parameter) {
        // Escape all parameter values one by one when there are multiple values
        return Arrays.stream(super.getParameterValues(parameter)).map(this::clean).toArray(String[]::new);
    }
    @Override
    public String getHeader(String name) {
        // Also clean the request headers
        return clean(super.getHeader(name));
    }
    @Override
    public String getParameter(String parameter) {
    
```java
// Handling single parameter values

return clean(super.getParameter(parameter));

}

// The clean method is used to HTML escape the value

private String clean(String value) {

 return StringUtils.isEmpty(value)? "" : HtmlUtils.htmlEscape(value);

}

}

This way, we can achieve HTML escaping for all request parameters. However, this approach is still not thorough enough because it cannot handle JSON data submitted through the @RequestBody annotation. For example, consider a PUT endpoint that directly saves the JSON User object passed in by the client:

@PutMapping

public void put(@RequestBody User user) {

 userRepository.save(user);

}

When making a request to this endpoint using Postman, the data saved in the database is still not escaped:

![img](../images/6d8e2b3b68e8a623d039d9d73999a64f.png)

We need to customize a Jackson deserializer to escape the strings during deserialization:

// Register custom Jackson deserializer

@Bean

public Module xssModule() {

  SimpleModule module = new SimpleModule();

  module.module.addDeserializer(String.class, new XssJsonDeserializer());

  return module;

}

public class XssJsonDeserializer extends JsonDeserializer<String> {

  @Override

  public String deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JsonProcessingException {

    String value = jsonParser.getValueAsString();

    if (value != null) {

      // HTML escape the value

      return HtmlUtils.htmlEscape(value);

    }

    return value;

  }

  @Override

  public Class<String> handledType() {

    return String.class;

  }

}

This way, we can escape not only the data submitted via GET/POST request parameters, but also the JSON data submitted directly in the request body.

You may think that at this point, our defense is already comprehensive enough. However, that is not the case. This approach is only blocking new vulnerabilities and ensures that new data is escaped before entering the database. If some JavaScript code has already been saved in the database due to previous vulnerabilities, we could still encounter issues when reading the data. Therefore, we also need to escape the data when reading it.

Next, let's take a look at the specific implementation.

First, as we have addressed the JSON deserialization issue, we also need to handle serialization to escape the strings when reading from the database. Otherwise, the retrieved JSON may still contain JavaScript code.

For example, suppose we have a GET endpoint that returns user information in JSON:

@GetMapping("user")

@ResponseBody

public User query() {

  return userRepository.findById(1L).orElse(new User());

}

![img](../images/b2f919307e42e79ce78622b305d455f8.png)

Modify the previous SimpleModule to include a custom serializer and implement string escaping during serialization:

// Register custom Jackson serializer

@Bean

public Module xssModule() {

  SimpleModule module = new SimpleModule();

  module.addDeserializer(String.class, new XssJsonDeserializer());

  module.addSerializer(String.class, new XssJsonSerializer());

  return module;

}

public class XssJsonSerializer extends JsonSerializer<String> {

  @Override

  public Class<String> handledType() {

    return String.class;

  }

  @Override

  public void serialize(String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {

    if (value != null) {

      // HTML escape the string

      jsonGenerator.writeString(HtmlUtils.htmlEscape(value));

    }

  }

}

As a result, even if JavaScript code is already saved in the database, it will only be displayed as HTML. Now, we have implemented escaping in both the input and output directions.

However, to ensure we don't overlook anything and further control the potential harm caused by XSS, we need to consider another scenario: if we need to write sensitive information to cookies, we can enable the HttpOnly attribute. This way, JavaScript code cannot read the cookies even if the page is injected with XSS attack code.

Let's test it with some code. Define two endpoints: readCookie endpoint to read the cookie with the key "test", and writeCookie endpoint to write the cookie. The HttpOnly parameter determines whether the cookie has the HttpOnly attribute:

// Server-side code to read the cookie

@GetMapping("readCookie")

@ResponseBody

public String readCookie(@CookieValue("test") String cookieValue) {

  return cookieValue;

}

// Server-side code to write the cookie

@GetMapping("writeCookie")

@ResponseBody

public void writeCookie(@RequestParam("httpOnly") boolean httpOnly, HttpServletResponse response) {

  Cookie cookie = new Cookie("test", "zhuye");

  // Enable the HttpOnly attribute based on the httpOnly parameter

  cookie.setHttpOnly(httpOnly);

  response.addCookie(cookie);

}

We can see that due to "test" and "_ga" cookies not having HttpOnly, they can be accessed using document.cookie:

![img](../images/726e984d392aa1afc6d7371447700977.png)

After enabling the HttpOnly attribute for the "test" cookie, it cannot be accessed using document.cookie anymore, and only the "_ga" cookie is included in the output:

![img](../images/1b287474f0666d5a2fde8e9442ae2e0c.png)

However, the server can still access the cookie:

![img](../images/b25da8d4aa5778798652f9685a93f6bd.png)
## Key Takeaways

Today, I analyzed two types of injection security issues, SQL injection and XSS attacks, through case studies.

While learning about SQL injection, we saw several common injection methods using the sqlmap tool. This may have changed our understanding of the power of SQL injection: even for POST requests with no returned data and no errors, it is still possible to complete the injection and export all data from the database.

For SQL injection, using parameterized queries is the best way to prevent leaks. For JdbcTemplate, we can use "?" as a parameter placeholder, while for MyBatis, we need to use "#{}" for parameterization.

Similar to SQL injection, when dynamically executing code with a script engine, we need to ensure that externally provided data is treated only as data and not concatenated with code. The boundaries between code and data need to be clear, otherwise code injection issues may arise. Additionally, we can refine the permissions of the code by setting up a code execution sandbox, making it difficult for injection attacks to have a significant impact due to restricted privileges.

Next, through studying XSS cases, we realized that handling security issues requires ensuring three things.

First, fixing the leak from the root and at the lowest level is essential. It is best to avoid fixing the issue at a higher-level framework, as it may not be thorough enough.

Second, fixing the leak should consider both input and output. We need to ensure that data is properly escaped or filtered when it is stored in the database and when it is presented, to ensure complete security.

Third, in addition to direct fixes, we can also limit the impact of vulnerabilities through additional means. For example, setting the HttpOnly attribute for cookies prevents data from being read by scripts. Similarly, limiting the maximum length of fields can restrict a hacker's ability to construct complex attack scripts, even if a vulnerability exists.

The code used today is available on GitHub. You can click on this [link](https://github.com) to view it.
## Reflection and Discussion

In discussing SQL injection cases, in the last test, we saw that sqlmap returned 4 types of injection methods. I have already introduced boolean-based blind injection, time-based blind injection, and error-based injection. Do you know what union-based injection is?

When discussing XSS, we know how to escape HTML and display it as text in Thymeleaf template engine. FreeMarker is also a commonly used template engine in Java. Do you know how to handle escaping in FreeMarker?

Have you encountered any other types of injection issues? I am Zhu Ye. Feel free to leave a comment in the comment section to share your thoughts with me. You are also welcome to share today's content with your friends or colleagues and discuss together.