DEV Community

loading...

Passing IDs as numbers? You are under the risk!

alekseiberezkin profile image Aleksei Berezkin ・4 min read

Here's the trivial Java–JS interaction to find a user by name, and it contains a severe problem. Can you spot one?

@RestController
public class SearchController {
    @GetMapping("/findUser")
    public UserInfo findUser(String name) {
        return elasticFacade.findUser(name);
    }
}

public class UserInfo {
    public long id;
    public String name;
}
Enter fullscreen mode Exit fullscreen mode
export const findUser = name =>
    fetch(`/findUser?name=${name}`)
    .then(r => r.json())
    .then(({id, name}) => setUserInfo({id, name}));
Enter fullscreen mode Exit fullscreen mode

When language matters

Debates what language is best will never end. Some people like Java simplicity; others say there's nothing better than JS functions. However, much languages allow writing awesome software for a variety of applications — frontend, backend, desktop, ML, and many more. But... There's something you cannot ignore, and which is quite hard to emulate or workaround: language primitive types, especially numbers.

Java has a variety of primitive numbers to choose from:

  • integer
    • byte: signed 8-bit
    • char: unsigned 16-bit, mainly used for UTF-16 codes
    • short: signed 16-bit
    • int: signed 32-bit
    • long: signed 64-bit
  • floating-point
    • float: 32-bit
    • double: 64-bit

JavaScript has only two number primitives:

  • number — the “default” type
  • bigint — it's quite new, so JS uses it only if you ask explicitly with n suffix, like 42n. All traditional APIs and applications like JSON, DOM, CSS use simple number. This also means all numbers passed into JS are coerced to number.

What is number exactly? This is my favorite question I ask interviewing for fullstack positions. Surprisingly, few candidates know, which is very sad. Do you know the answer? 🙂

The number is...

A 64-bit floating point number, just like double of Java, C++ and C#. So any other number without n suffix is converted into this type. Can it hold all numbers which Java and C# can pass, including the largest from long range? To answer this question we need to understand how these types are stored in memory. That's not that hard, so let's dive in!

long

It's quite simple: higher bit stores the sign (0 = positive 1 = negative), others store the value.

partition | sign |         value         |
bit       |   63 | 62 | 61 | ... | 1 | 0 |
Enter fullscreen mode Exit fullscreen mode

When the number is negative, the value is encoded in so called “2s complimentary” code, but let's leave it for really curious folks 😉 That's how the positive long is interpreted:

value=262bit62+261bit61+...+2bit1+1bit0 value = 2^{62} ⋅ bit_{62} + 2^{61} ⋅ bit_{61} + ... + 2 ⋅ bit_{1} + 1 ⋅ bit_{0}

The largest long is when all bits except the sign are ones, and this gives 9,223,372,036,854,775,807.

number and double

The type is designed to represent numbers of different magnitudes, including very large, like the size of the Universe, and very small, like distances between atoms. These numbers are usually written with so called “scientific notation”:

x=1.53191035y=8.140381021 \begin{aligned} x &= 1.5319 ⋅ 10^{35} \\ y &= 8.14038 ⋅ 10^{-21} \end{aligned}

This notation have two parts: the significand (or “fraction”) and the exponent (1.5319 and 35 respectively for xx ). Floating-point binary representation mirrors this structure also having these partitions:

partition | sign |   exponent    | significand  |
bit       | 63   | 62 | ... | 52 | 51 | ... | 0 |
Enter fullscreen mode Exit fullscreen mode

When the exponent is 0, the number is interpreted in this way:

value=12+122bit51+123bit50+...1253bit0 value = {1 \over 2} + {1 \over 2^2} ⋅ bit_{51} + {1 \over 2^3} ⋅ bit_{50} + ... {1 \over 2^{53}} ⋅ bit_{0}

But can it store larger and smaller numbers? That's where the exponent comes into play! When the exponent is expexp , it literally says “please multiply the whole significand by 2exp2^{exp} ”.

Now, recall our example. We wanted to store a long which is 2622^{62} in the upper bit, so to get the first summand equal to 2622^{62} we need multiplying the value by 2632^{63} :

exp=63value=262+261bit51+260bit50+...+210bit0 \begin{aligned} exp &= 63 \\ value &= 2^{62} + 2^{61} ⋅ bit_{51} + 2^{60} ⋅ bit_{50} + ... + 2^{10} ⋅ bit_{0} \end{aligned}

That's very similar to long formula, but... where are summands less than 2102^{10} ? We need them but there are no more bits and the precision suffers 😥 To get it back we need to decrease expexp to no more than 53:

exp=53value=252+251bit51+250bit50+...+1bit0 \begin{aligned} exp &= 53 \\ value &= 2^{52} + 2^{51} ⋅ bit_{51} + 2^{50} ⋅ bit_{50} + ... + 1 ⋅ bit_{0} \end{aligned}

Now the precision is back but seems like we lost the ability to represent the full long range 😕 What can we do with it? Just accept it, and always keep in mind.

So, number allows...

  • Either having large but imprecise number
  • Or having precise but limited integer. This limit is so important that has its own name: MAX_SAFE_INTEGER.

Feel the precision loss

Just open the console right on this page and try to output the largest long:

console.log(9223372036854775807)
VM139:1     9223372036854776000
Enter fullscreen mode Exit fullscreen mode

If the argument is for example a physical distance we may assume it was just rounded a bit. Come on, it's 9 quintillion meters, who cares about couple of kilometers error!

But what if it's someone's id? You got the wrong user! If the code like this runs on a backend you compromise the privacy!

What can I do?

Never, never ever pass long IDs as numbers to a JS code. Convert them to strings!


Thanks for finishing this reading. Have you fixed issues like this? Share your examples! If you find this material helpful please consider leaving some feedback. Thanks!

Discussion

pic
Editor guide