JWT (JSON Web Token) (in)security

JWT (JSON Web Token) is a mechanism that is often used in REST APIs it can be found in popular standards, such as OpenID Connect, but we will also encounter it sometimes using OAuth2. It is used both in large companies and smaller organisations. There are many libraries available that support JWT, and the standard itself has “rich support for cryptographic mechanisms”. Does all this mean JWT is inherently safe? Let’s see.

Introduction to JWT

RFC 7519 says:

JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure (…)

Simply put, JWT is a sequence of characters in JSON format (https://www.json.org/) encoded in JWS (JSON Web Signature) or JWE (JSON Web Encryption) structure. Besides, each of these options must be serialized in a compact way (one of the two serializations in JWS and JWE). Most often you can see JWS, and it is this structure that is popularly called JWT. In turn, “claim” is usually a simple pair of “key” : “value”.

An example of a JWT is shown below:

You can see we have here three long strings of characters separated by a dot. The structure of the whole is as follows:

header . payload . signature

The above three elements are encoded with BASE64URL algorithm (which looks very similar to BASE64, where the plus (+) sign in the output is replaced by minus (-), slash (/) is replaced by underline (_), and there is no standard BASE64 padding, which usually consists of equal signs (=)).

After decoding the above sequence (which can be done, for example, via https://jwt.io/), we can see a fully readable header and payload.

If you want to learn more about JWT, the following resources might be useful:

  1. Introduction.
  2. More technical details.
  3. JOSE Project (Javascript Object Signing and Encryption) – except for JWT / JWT you can read more about JWK (JSON Web Key), JWA (JSON Web Algorithms), different usage of the mechanisms in OAuth2 / OpenID Connect; it turns out that JWS can have more than one signature, JWTs can be nested…
    Nice summary is available here.


Let’s move on to our main topic – JWT security. JWTs can be sometimes used as “API keys“. En example of such a key may look like this:


After decoding with the BASE64URL-decode function, we have the following sections:


“alg “: “HS256 “,
“typ “: “JWT “


“iat”: “1416929061”,
“jti”: “802057ff9b5b4eb7fbb8856b6eb2cc5b”,
“scopes”: {
“users”: {
“actions”: [
“users_app_metadata”: {
“actions”: [


Just binary content

As you can see, with this “API key” (its main content is in the payload), we can have implemented both authentication (I have the privilege to communicate with API) and authorization (in the payload above you can see example actions that can be performed by the owner of the key).

From the security perspective, there are at least two potential problems, to begin with.

The first one is the lack of confidentiality – we were able to decode payload (and header) easily. Sometimes, it is not a problem (or it’s even an advantage), but when we require confidentiality of the data sent in the token, there is a better way to do it: JWE (JSON Web Encryption).

The second problem is a potential possibility of inserting another action by the user – e.g. delete – and thus bypassing the authorization. In this case, the solution is using signatures in tokens (note that in the above example, we saw a “signature”). The HS256 algorithm indicated in the header is a standard HMAC-SHA256 – a mechanism that ensures the integrity of the whole message (thanks to it, the user cannot change the payload; or else – he can, but the API which accepts the token will detect tampering during signature verification). To configure HS256, you need to generate a key (string) and place it in the API configuration.

For the sake of clarity of the message, it is possible to imagine the signature in a very simple way:

SHA-256(header || payload || key) – where || means concatenation

Warning: formally speaking, it is a bit more complicated: we use HMAC-SHA256 algorithm to which we give a key and a message equal to the result:

BASE64URL(UTF8(JWS Protected Header)) || ‘.’ || BASE64URL(JWS Payload)

If someone changes the payload, he cannot generate a new signature (because he needs a key for it, which is in the API configuration only).

To sum up, the JWT looks much more flexible than the API keys – you can easily transfer any data, ensure its integrity and, if necessary, maintain confidentiality. Additionally, all the information (except the secret key) can be in the token itself. However, there is no rose without thorns.

First thorn: it’s complicated!

One of the main problems is that the JWT is a very complex mechanism. JWT / JWS / JWE / JWK, a multitude of cryptographic algorithms, two different ways of encoding (serialization), compression, the possibility of more than one signature, encryption to multiple recipients – these are just a few examples. All JWT related specifications have 300+ pages! The complexity is certainly not a friend of security.

Second Thorn: none

As I’ve already mentioned, it is often assumed that the “proper” JWT is a JWT with a signature (JWS), although, according to the formal specification of JWT, a signature is not mandatory. The relevant RFC document indicates so-called “unsecured JWT”, (ie. without signature). How such unsigned JWT looks like? In the header, we have:

“alg “: “none “,
“typ “: “JWT “

In the payload, the usual stuff. The signature section is empty (or can be ignored by the token processor). Interestingly, “none” algorithm is one of two the algorithms that MUST be implemented according to the RFC:

Of the signature and MAC algorithms specified in JSON Web Algorithms [JWA], only HMAC SHA-256 (“HS256”) and “none” MUST be implemented by conforming JWT implementations.

What does that give the attacker? Well, he can obtain a JWT (with a signature), change it (e.g. add new permission), then put this in the header {“alg”:”none”}. Then send the whole thing to the API with or without a signature. Should the server accept such a token? Theoretically yes, but it would destroy of the whole idea of JWT signatures. However, such situations really took place (maybe still do?) in many libraries implementing JWTs: https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/, https://www.cvedetails.com/cve/CVE-2018-1000531/.

By the way, it is worth mentioning another problem: what happens if we have a signature algorithm in the header (e.g. HS256 or HS512), but we remove the entire signature section from the token?

As you can see here, sometimes such a token will be verified correctly! You can say a little ironically – this is a modern version of the “none” problem (see Picture below):

(…) an input validation vulnerability in JWTDecoder.decode that can result in a JWT that is decoded and thus implicitly validated even if it lacks a valid signature.

Sometimes, there is another problem – if the attacker does not know how to create a proper signature, maybe it will be inserted in an error message? Unbelievable? Let’s see this vulnerability:

Critical Security Fix Required: You disclose the correct signature with each SignatureVerificationException.

So if someone changed the payload and sent such a token to the server, the server politely informed us about it, giving a correct token that matches our payload:

To make it work, the server had to be configured to display exceptions to the user, but this is not a very rare (although incorrect) configuration. Similar problem was discovered in another library:

All versions of Auth0-WCF-Service-JWT NuGet package lower than 1.0.4 include sensitive information about the expected JWT signature in an error message emitted when JWT signature validation fails.

Third thorn: cracking the HMAC key

Let’s go back to the signature, namely the HS256 algorithm (HMAC-SHA256). How is the signature calculated? As we’ve seen a bit earlier:

How to recover the JWT secret (and have ability to create a valid signature for any JWT)? The standard approach takes a token generated by API and runs classic brute-force/dictionary/hybrid attack. One iteration requires calculating two SHA256 hashes (this is how HMAC-SHA256 works), and there are also tools that automate the whole operation, such as hashcat that implements cracking the JWT key using GPU(s). With a couple of of speedy GPUs, you can achieve speeds of over a billion checks per second. What is more, the whole operation can be done offline without any interaction with the API (it is enough to get one arbitrary token with a signature). An example of a JWT cracking session looks like this:

Session……….: hashcat
Status………..: Running
Hash.Type……..: JWT (JSON Web Token)
Hash.Target……: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMj…
Guess.Mask…….: ?1?2?2?2?2?2?2 [7]
Guess.Charset….: -1 ?l?d?u, -2 ?l?d, -3 ?l?d*!$@_, -4 Undefined
Guess.Queue……: 7/15 (46.67%)
Speed.Dev.#1…..: 198.0 MH/s (9.68ms) @ Accel:32 Loops:8 Thr:512 Vec:1
Recovered……..: 0/1 (0.00%) Digests, 0/1 (0.00%) Salts
Progress………: 17964072960/134960504832 (13.31%)
Rejected………: 0/17964072960 (0.00%)
Restore.Point….: 0/1679616 (0.00%)
Candidates.#1….: U7veran -> a2vbj14

On a single Nvidia GTX 1070 card, we get a breaking speed of about 200 million checks per second. If the hashcat manages to break the key, you get a result as below (where secrety is the key you are looking for):


As you can see, too weak key set in the configuration can lead to its recovery, and, consequently, the attacker can generate any (properly verified) tokens. How much complex key should we use? The answer is buried in the relevant RFC (JSON Web Algorithms):

A key of the same size as the hash output (for instance, 256 bits for “HS256”) or larger MUST be used with this algorithm. (This requirement is based on Section 5.3.4 (Security Effect of the HMAC Key) of NIST SP 800-117 [NIST.800-107], which states that the effective security strength is the minimum of the security strength of the key and two times the size of the internal hash value.).

By the way, it is worth emphasising once again that breaking the JWT key can be done completely offline.

Forth thorn: too many cooks spoil the JWT broth

Many JWT security problems arise as an outcome of implementing the (really complex) standard. Let’s see an example.

How can we choose the signature algorithm in JWS? So far, I’ve mentioned HMAC (and only with SHA256 function), but it’s not the only option. An overview of the various signature variants can be found here: https://auth0.com/blog/json-web-token-signing-algorithms-overview/.

A common option is to use an asymmetric algorithm – RSA. In this case, we will see in the header “alg”: “RS512” or “alg”: “RS256”.

Just a little reminder: an RSA private key is used for signatures, and a public key associated with it can verify signatures. So in this case instead of a symmetric key (as it was in HS256 algorithm), we generate a pair of RSA keys.

By the way, If you first see RS512 or RS256, you might think of a requirement to use 512 or 256 bit RSA keys? Such keys are breakable at minimal cost and time (see: https://eprint.iacr.org/2015/1000.pdf or https://www.theregister.co.uk/2010/01/07/rsa_768_broken/). Currently, even a 1024-bit RSA key is not considered secure. Fortunately, this only points to the specific SHA function used in connection with RSA. For example RS512 means RSA plus SHA512 function. But what about the RSA key? The length is set by the person generating it, which is another potential problem (moreover in different online tutorials you can find specific commands using OpenSSL and generating 1024-bit keys). It is also worth remembering that using RSA will have an impact on the efficiency of the entire system (signature verification), so in this case, we choose a slower variant than HS256.

Coming back to the point, with RSA algorithm, we have at least one more interesting security problem. As I wrote earlier, a public key is used for signature verification, so it will usually be set in the API configuration as the verification_key. Here, it is worth noting that for HMAC, we only have one symmetric key that is used for both signing and verification.

How an attacker can forge a JWT token?

  1. He obtais a public key (its very name indicates that it might be available publicly). Sometimes, it is transmitted within the JWT itself.
  2. Sends the token (with changed payload) with the HS256 algorithm set in the header (i.e. HMAC, not RSA) and sign the token with the public RSA key. Yes, there is no mistake here – we use the public RSA key (which we give in the form of a string) as a symmetric key to HMAC.
  3. The server receives token, checks which algorithm was used for the signature (HS256). The verification key was set in the configuration as the public RSA key, so…:
  4. The signature is validated (because exactly the same verification key was used to create the signature, and the attacker set the signature algorithm to HS256).

What was the problem here? The fact that it’s possible to provide the signature algorithm by the user, although we intended to verify the signature of the token using RSA only. So either we force only one selected signature algorithm (we don’t give the possibility to change it by changing the token) or let’s provide separate verification methods (and keys!) for each signature algorithm we support. Here is a real example of this vulnerability:

Since “algorithm” isn’t enforced in jwt.decode() in jwt-simple 0.3.0 and earlier, a malicious user could choose what algorithm is sent sent to the server.

If the server is expecting RSA but is sent HMAC-SHA with RSA’s public key, the server will think the public key is actually an HMAC private key. This could be used to forge any data an attacker wants. 

Fifth thorn: though perhaps it should be given a priority

Sometimes, you can provide your own key in the token, which will then be used by the API to verify it! Sounds unbelievable, right? Then see details of the vulnerability of CVE-2018-0114 in the node-jose library: https://tools.cisco.com/security/center/viewAlert.x?alertId=56326.

The vulnerability is due to node-jose following the JSON Web Signature (JWS) standard for JSON Web Tokens (JWTs). This standard specifies that a JSON Web Key (JWK) representing a public key can be embedded within the header of a JWS. This public key is then trusted for verification. An attacker could exploit this by forging valid JWS objects by removing the original signature, adding a new public key to the header, and then signing the object using the (attacker-owned) private key associated with the public key embedded in that JWS header.

The problem is not new, earlier (2016), a similar vulnerability was detected in the Go-jose library (https://mailarchive.ietf.org/arch/msgif/jose/gQU_C_QURVuwmy-Q2qyVwPLQlcg):

RFC 7515, https://tools.ietf.org/html/rfc7515#section-4.1.3 “jwk” (JSON Web Key) Header Parameter allows the signature to include the public key that corresponds to the key used to digitally sign the JWS. This is a really dangerous option.

One another example of this is here (perl-Crypt-JWT):

ADDED in 0.23

1 – use “jwk” header value for validating JWS signature if neither
“key” nor “kid_keys” specified, BEWARE: DANGEROUS, UNSECURE!!!
0 (default) – ignore “jwk” header value when validating JWS signature

Sixth thorn: can JWT encryption even work?

What about encryption (JSON Web Encryption)? Here, you can choose from several algorithms (encryption of the message itself or encryption of the symmetric key used to encrypt the message). Again, there is some interesting research, namely several implementations of the JWE have made it possible for the attacker to recover the private key: https://blogs.adobe.com/security/2017/03/critical-vulnerability-uncovered-in-json-encryption.html. More specifically, there was a problem in the implementation of the ECDH-ES algorithm (which, by the way, has the status “Recommended+” in the relevant RFC document: https://tools.ietf.org/html/rfc7518).

Maybe the previous vulnerability was just an accident? Let’s see AES in the GCM mode: https://rwc.iacr.org/2017/Slides/nguyen.quan.pdf. Google researchers are writing about this in context of JWT, and they sum up: GCM is fragile, but its implementations were rarely checked.

We can also choose the RSA algorithm with PKCS1v1.5 padding. What’s wrong with it? The problems have been known since 1998: ftp://ftp.rsa.com/pub/pdfs/bulletn7.pdf. And some people sum it up like this:

PKCS#1v1.5 is awesome – if you’re teaching a class on how to attack cryptographic protocols.

More details about possible algorithms and possible problems can be found here:

» https:/paragonie.com/blog/2017/03/jwt-json-web-tokens-is-bad-standardthat-everyone-should-avoid
»  https://paragonie.com/blog/2016/12/everything-you-know-about-public-keyencryption-in-php-is-wrong
»  https://paragonie.com/blog/2018/04/protecting-rsa-based-protocols-againstadaptive-chosen-ciphertext-attacks

Will we always be doomed to failure by using JWE? Of course not, but it is worth verifying if we use a properly secure encryption algorithm (and its secure implementation).

Seventh thorn: decoding/verification – does it matter? 😉

Now we can feel a bit overwhelmed by the multitude of options. After all, we only want to “decode” the token on the API side and use the information contained in it. Remember, however, that “decode” does not always mean the same as “verify” – supposedly obvious, but different libraries may provide different functions for decoding and/or verifying tokens. An example of this type of question or doubt can be found here.

In short, if I use decode() function I might only decode payload (or header) from BASE64URL without any verification. Verification can be a separate function, although it can be built into a decode() too. Sometimes, it is the users who ask for such an option – in the case quoted below – someone asks to to overload the decode() method so that it can also accept the token itself (without the key):

I have seen an issue in the framework to get the payload of a JWT. To get a payload of a JWT without validation, we don’t need a key/secret. So, in the file jwt/src/JWT/JwtDecoder.cs, we missed an overload for the Decode method that only needs a token.

If the programmer, using the library, now uses the simplest call – decode(token), the signature will not be verified.

Additionally, in the discussion quoted above there is a question about the possibility of verifying the signature on the client side, which, as we already know in the case of HS256 can be tricky (client must have a secret key, which can be extracted by an attacker). Unfortunately, often the use of JWT boils down to the following: let’s choose the first algorithms we see and copy the pieces of code from tutorials. Does it work? Yeah, so what’s the problem?

Eighth thorn: capture any token = take over API access?

One of the often pointed out advantages of JWT is the implementation of authentication (or authorization – depends on the context in which the whole will be used) without the need to execute a query to the database. Moreover, we can do this in parallel on several independent servers (APIs). After all, the content of the token alone is enough to make a decision here. It also has a disadvantage – what if the signing key available on many servers somehow leaks? Of course, it will be possible to generate properly signed tokens accepted by all machines that use the appropriate key for verification. What can an attacker gain by doing so? For example, unauthorized access to API functions or other user accounts.

We also have a potentially different problem here – what if one generated token can be used in many different contexts? For example, we generate a valid token with the content of { “login” = “manager” }. And in one API function it gives the possibility to edit some data, but at the same time, we forgot that a completely different function, receiving the same token, gives the possibility of full access!

In this case, it is possible to use certain parameters defined by the specification itself: iss (issuer) and aud (audience). Thanks to them, tokens can then be accepted only by our specific recipients.

For example:

“iss ” = “my_api “,
“login ” = “manager “,
“aud ” = “store_api “

There is another problem we can point out – some reserved keywords in JWT (Registered Claim Names) – e.g. iss/aud/iat/jti – can be placed next to any element defined by the JWT user – e.g. login from our example. This is causing another confusion, which I will point out in the next problem.

Ninth thorn: replay JWT

What if a specific token should be used only once? Let’s imagine a scenario when a user writes a generated token to execute the DELETE method in our API. Then, e.g. after a year – when he theoretically no longer has the appropriate permissions – he tries to use it again (the so-called replay attack).

The way to do this is to use the following claims: jti and exp. Jti (JWT ID) is a token identifier, which must be unique, and exp is a definition of the expiration date of a token. The combination of both fields will give us an appropriately short validity of the token and its uniqueness.

It is worth noting, however, whether we have a correct implementation of these halves. Now look at the bug in which the exp value was not taken into account at all (https://github.com/jwt-dotnet/jwt/issues/134).

JWT not throwing ExpiredTokenException in .NetCore

Library developers used expiry claim (which isn’t in JWT specification) to perform expiry checks; the bug was corrected after reporting.

Tenth thorn: timing attacks on the signature

If the signature from JWS is checked by a byte after byte with the correct signature (generated by the party which accepts JWS), and if the verification finishes on the first inconsistent byte, we may be susceptible to a time attack.

Note that in such a case, the more matching bytes we have, the more comparisons are needed, and thus the longer the time needed for the response.

It is possible to observe the response times by generating successive signatures, starting from the first byte of the signature, then moving on to the second one, etc. The exact description of such an attack can be seen here: https://hackernoon.com/can-timing-attack-be-a-practical-security-threat-on-jwt-signature-ba3c8340dea9.

According to the report, with a large amount of generated traffic (up to 55 thousand requests per second), the signature of any message could be obtained in 22 hours (laboratory conditions). With less traffic, of course, we will need more time (several days), but the effect can be shocking (we can generate any JWT and prepare a signature that will be verified as correct).

Is the attack really possible in real life scenario? As you can imagine, the changes in response time are minimal, but you can try to measure them. At this point, it is also worth reminding a bit older text about time attacks: https://www.cs.rice.edu/~dwallach/pub/crosby-timing2009.pdf. The following measurements have been achieved:

Our work analyses the limits of attacks based on accurately measuring network response times and jitter over a local network and across the Internet. We present the design of filters to significantly reduce the effects of jitter, allowing an attacker to measure events with 15-100µs accuracy across the Internet and as good as 100ns over a local network.

On the other hand, an example of a declaration of a specific type of vulnerability can be found, e.g. here or here.

Eleventh thorn: multitude of libraries

As one of the first problems concerning JWT, I mentioned a multitude of available options and various algorithms. There is also a large number of often incomplete JWT implementations, written, to put it mildly, on the knee. Not easy to follow and sometimes problematic in terms of security standard requires the use of complex cryptographic algorithms by people who often do not have a deep knowledge of cryptography. Garbage in – garbage out: it is difficult to expect super-secure libraries here.

I have already listed some bugs in the libraries. Other examples here:
» https://auth0.com/docs/security/bulletins/cve-2019-13483
» https://www.cvedetails.com/cve/CVE-2017-12973/
» https://www.cvedetails.com/cve/CVE-2017-10862/
» https://pivotal.io/security/cve-2017-2773
» https://github.com/auth0/node-jsonwebtoken/issues/212
» https://github.com/auth0/node-jsonwebtoken/commit/ adcfd6ae4088c838769d169f8cd9154265aa13e0
» https://www.cvedetails.com/cve/CVE-2019-17195/

At the same time, it is worth mentioning security problems related to the used libraries in general. It is worth checking whether I use a library with known public vulnerabilities. Or maybe my libraries are using vulnerable dependencies? Do I have developed monitoring of the used library in case someone locates a significant vulnerability in the future?


Looking at many of the security problems I pointed out in the JWT, one might wonder if we have a proven alternative. At present, the answer is rather negative. One of the new ideas is PASETO:

Paseto is everything you love about JOSE (JWT, JWE, JWS) without any of the many design deficits that plague the JOSE standards.

Simply put, PASETO is to be a safe version of JWT. Will it really live up to the promises? At this time, it is really hard to say – it is a very young project and still in the development phase. More information can be found here and here. In the subject of motivations behind starting this project, we can read here.


The opinions in the security community are divided. Some people categorically discourage using JWT, others point to poorly prepared implementations, while others describe the JWT mechanisms themselves exactly as it is, leaving the decision to the user. Additionally JWT is a very popular mechanism, to which there is no standardized, popular alternative, which has additionally proven its safety.

The security problems I have indicated before can be placed in three categories:

  1. Problems with the JWT specification itself (e.g. none algorithm).
  2. Library implementation errors, including cryptographic algorithm implementation errors (it is probably the most numerous group).
  3. Incorrect use of the library.

Most of the common problems are presented below.

Let’s summarise the whole thing with practical advices that can increase JWT usage security. Some recommendations may contain thought abbreviations, which are explained earlier in the text, and additionally further covered in the linked source materials.

To begin with

1. Understand what you want to use: consider whether you need JWS or JWE, choose the appropriate algorithms, understand their purpose (at least on a general level – e.g. HMAC, public key, private key). Find out what exactly offers the JWT library you have chosen. Maybe there is a ready-made, more straightforward mechanism you can use?


2. Use appropriately complex symmetric/asymmetric keys.

3. Have a scenario prepared in case of compromise (disclosure) of one of the keys.

4. Keep the keys in a safe place (e.g. do not hardcode them permanently in the source code).

5. Ideally do not allow to set arbitrary signature algorithm by the sending party (it is best to force a specific signature algorithm(s) on the server side).


6. Check if your implementation does not accept the none signature algorithm.

7. Check if your implementation doesn’t accept an empty signature (i.e. the signature is not checked).

8. If you use JWE, check that you are using safe algorithms and that you are using safe implementation of these algorithms.

9. Distinguish between verify() and decode(). In other words, check if you are sure you are verifying the signature.

General rules

10. Check if the token generated in one place cannot be used in another to gain unauthorised access.

11. Check that the debug mode is turned off and that it cannot be activated with a simple trick (e.g. ?debug=true).

12. Avoid sending tokens in URLs (this might leak sensitive data – e.g. such tokens are then written to web server logs).


13. Check whether you are placing confidential information in JWS payload (not recommended).

14. Make sure you are protected against a replay attack (resending a token).

15. Make sure that the tokens have a sufficiently short validity period (e.g. by using the “exp” claim).

16. Make sure that the “exp” is actually checked. Think about whether you need to invalidate a specific token(s) (the standard does not give tools for this, but there are several ways to implement this type of mechanism).


17. Read the library’s documentation carefully.

18. Check the vulnerabilities in the library you use (e.g. in the service: cvedetails.com or on the project website).

19. Check that your previous projects do not use a vulnerable library; check if you are monitoring new bugs in the library (they may show up, e.g. after a month of implementation).

20. Track new vulnerabilities in libraries that support JWT. Perhaps, in the future, someone will find a vulnerability in another project, which exists in the same form in the library you are using.

Useful resources:

1. JSON Web Token Best Current Practices:


2. JWT Handbook:


3. Discussion on vulnerabilities of JWT:


4. JWT Cheat Sheet for Java (OWASP).


5. A couple of ideas on how to use JWT safer:


6. A set of arguments against using JWT to create a session:


7. Comparison of JWTs with session IDs and advice on relevant security features: http://by.jtl.xyz/2016/06/the-unspoken-vulnerability-of-jwts.html