Readit News logoReadit News
n2d4 · 10 months ago
The default behaviour of setTimeout seems problematic. Could be used for an exploit, because code like this might not work as expected:

    const attackerControlled = ...;
    if (attackerControlled < 60_000) {
      throw new Error("Must wait at least 1min!");
    }

    setTimeout(() => {
      console.log("Surely at least 1min has passed!");
    }, attackerControlled);

The attacker could set the value to a comically large number and the callback would execute immediately. This also seems to be true for NaN. The better solution (imo) would be to throw an error, but I assume we can't due to backwards compatibility.

arghwhat · 10 months ago
A scenario where an attacker can control a timeout where having the callback run sooner than one minute later would lead to security failures, but having it set to run days later is perfectly fine and so no upper bound check is required seems… quite a constructed edge case.

The problem here is having an attacker control a security sensitive timer in the first place.

a_cardboard_box · 10 months ago
The exploit could be a DoS attack. I don't think it's that contrived to have a service that runs an expensive operation at a fixed rate, controlled by the user, limited to 1 operation per minute.
chacham15 · 10 months ago
I would imagine the intent behind this would be that the attacker has indirect control over the timeout. E.g. a check password input which delays you in between attempts doubling the length of time you have to wait in between each failed attempt. With this bug in place, the attacker would simply wait all the timeouts until the timeout exceeded 25 days at which point they could brute force the password check back to back.
swatcoder · 10 months ago
That's just terrible input validation and has nothing to do with setTimeout.

If your code would misbehave outside a certain range of values and you're input might span a larger range, you should be checking your input against the range that's valid. Your sample code simply doesn't do that, and that's why there's a bug.

That the bug happens to involve a timer is irrelevant.

ashkankiani · 10 months ago
You are a bad programmer if you think silently doing the wrong thing is not a bug. The right thing to do with unexpected input as the setTimeout library author is to raise an exception.
paulddraper · 10 months ago
> If your code would misbehave outside a certain range of values and you're input might span a larger range, you should be checking your input against the range that's valid.

What's funny is you think that about the caller of setTimeout but not setTimeout itself :)

tourist2d · 10 months ago
> That's just terrible input validation and has nothing to do with setTimeout.

Except for the fact that this behaviour is surprising.

> you should be checking your input against the range that's valid. Your sample code simply doesn't do that, and that's why there's a bug.

Indeed, so why doesn't setTimeout internally do that?

wging · 10 months ago
In nodejs you at least get a warning along with the problematic behavior:

    Welcome to Node.js v22.7.0.
    Type ".help" for more information.
    > setTimeout(() => console.log('reached'), 3.456e9)
    Timeout { <contents elided> }
    > (node:64799) TimeoutOverflowWarning: 3456000000 does not fit into a 32-bit signed integer.
    Timeout duration was set to 1.
    (Use `node --trace-warnings ...` to show where the warning was created)
    reached
I'm surprised to see that setTimeout returns an object - I assume at one point it was an integer identifying the timer, the same way it is on the web. (I think I remember it being so at one point.)

dgoldstein0 · 10 months ago
It's return your differs between node and in a browser. If you want to type a variable to hold the return value in typescript and share that across node (eg jest tests where you might include @types/node) and the browser you need ReturnType<typeof setTimeout>, otherwise the code won't typecheck in all cases. Similar with setInterval.
augusto-moura · 10 months ago
It returns an object for a long time now, I might say it was always like this actually. Don't know about very old versions
jandrese · 10 months ago
One could imagine an app that doubles the wait between each failed authentication attempt could exploit this by doggedly trying until the rate limiter breaks. Maybe not the most practical attack, but it is a way this behavior could bite you.
userbinator · 10 months ago
I always try to force the timeout to 0 on those really annoying download sites that try to make me wait.

Sometimes the wait is over before I find the responsible code, and sometimes it does check server-side, but that's just part of the fun...

sfvisser · 10 months ago
Don’t ever use attacker controlled data directly in your source code without validation. Don’t blame setTimeout for this, it’s impolite!
n2d4 · 10 months ago
The problem is the validation. You'd expect you just have to validate a lower bound, but you also have to validate an upper bound.
jackconsidine · 10 months ago
This type of thing is actually practical. Google Cloud Tasks have a max schedule date of 30 days in the future so the typical workaround is to chain tasks. As other commenters have suggested you can also set a cron check. This has more persistent implications on your database, but chaining tasks can fail in other ways, or explode if there are retries and a failed request does trigger a reschedule (I hate to say I’m speaking from experience)
Waterluvian · 10 months ago
True. Though if you have a need to trigger something after that much time, you might recognize the need to track that scheduled event more carefully and want a scheduler. Then you’ve just got a loop checking the clock and your scheduled tasks.
jackconsidine · 10 months ago
Right on. Pretty quickly that's the better solution
yifanl · 10 months ago
If we're pedantic, this doesn't actually do what's advertised, this would be waiting X timeouts worth of event cycles rather than just the one for a true Big timeout, assuming the precision matters when you're stalling a function for 40 days.
keithwhor · 10 months ago
I haven’t looked at the code but it’s fairly likely the author considered this? eg the new timeout is set based on the delta of Date.now() instead of just subtracting the time from the previous timeout.
yifanl · 10 months ago
No, it pretty much just does exactly that.

    const subtractNextDelay = () => {
      if (typeof remainingDelay === "number") {
        remainingDelay -= MAX_REAL_DELAY;
      } else {
        remainingDelay -= BigInt(MAX_REAL_DELAY);
      }
    };

gnachman · 10 months ago
That wouldn't very well because Date.now() isn't monotonic.
ericyd · 10 months ago
I don't understand how an implementation detail means it isn't doing what is advertised?
ballenf · 10 months ago
Each subtracted timeout is a 25 day timer, so any accumulated error would be miniscule. In your example there would a total of 2 setTimeouts called, one 25 day timer and one 15 day. I think the room for error with this approach is smaller and much simpler than calculating the date delta and trying to take into account daylight savings, leap days, etc. (but I don't know what setTimeout does with those either).

Or maybe I'm missing your point.

zeroonetwothree · 10 months ago
You don’t need to take into account daylight savings or leap days when dealing with unixtime.
n_plus_1_acc · 10 months ago
In response to this, I read the spec of setTimeout, bu I couldn't find the part where implementations may have an upper bound. Can someone more familiär with the specs point me in the right direction?
zeven7 · 10 months ago
Adding a comment here to check back later because I'm curious now if someone has the answer. I thought it would be easy to find the answer, but I can't find it either. I figured it would say somewhere a number is converted to an int32, but instead I got to the part where there's a map of active timers[1] with the time stored as a double[2] without seeing a clear loss happening anywhere before that.

[1] https://html.spec.whatwg.org/multipage/timers-and-user-promp...

[2] https://w3c.github.io/hr-time/#dom-domhighrestimestamp

vilius · 10 months ago
Here’s a deep dive in 6 minutes https://youtu.be/boD0ReK62FI?si=jSXuQn0DHn3riJgd

Just JS being JS: setTimeout(()=>{}, Infinity) executes immediately

n_plus_1_acc · 10 months ago
Thanks, but I'm looking for the specification of this behaviour.
ipython · 10 months ago
Sounds a lot like the famous windows 95 bug when it would crash after 49.7 days of uptime [1]

[1] https://news.ycombinator.com/item?id=28340101

sehugg · 10 months ago
GetTickCount() still exists and still returns a DWORD.
yesco · 10 months ago
> In most JavaScript runtimes, this duration is represented as a 32-bit signed integer

I thought all numbers in JavaScript were basically some variation of double precision floating points, if so, why is setTimeout limited to a smaller 32bit signed integer?

If this is true, then if I pass something like "0.5", does it round the number when casting it to an integer? Or does it execute the callback after half a millisecond like you would expect it would?

arp242 · 10 months ago
You're correct about JS numbers. It works like this presumably because the implementation is written in C++ or the like and uses an int32 for this, because "25 days ought to be enough for everyone".
drdaeman · 10 months ago
I thought most non-abandoned C/C++ projects have long switched to time_t or similar. 2038 is not that far in the future.
DvdGiessen · 10 months ago
When implementing a tiny timing library in JS a few years back I found that most engines indeed seem to cast the value to an integer (effectively flooring it), so in order to get consistent behaviour in all environments I resorted to always calling Math.ceil on the timeout value first [1], thus making it so that the callbacks always fire after at least the given timeout has passed (same as with regular setTimeout, which also cannot guarantee that the engine can run the callback at exactly the given timeout due to scheduling). Also used a very similar timeout chaining technique as described here, it works well!

[1]: https://github.com/DvdGiessen/virtual-clock/blob/master/src/...

blixt · 10 months ago
JS numbers technically have 53 bits for integers (mantissa) but all binary operators turns it into a 32-bit signed integer. Maybe this is related somehow to the setTimeout limitation. JavaScript also has the >>> unsigned bit shift operator so you can squeeze that last bit out of it if you only care about positive values: ((2*32-1)>>>0).toString(2).length === 32
tubs · 10 months ago
I assume by binary you mean logical? A + b certainly does not treat either side as 32bit.
purplesyringa · 10 months ago
Interestingly, this library seems to suffer from the opposite problem: where setTimeout can trigger earlier than expected, setBigTimeout can trigger never at all!

The problem is that when setBigTimeout is invoked with a floating-point number (and numbers are floating-point in JS by default), it keeps computing the time left till trigger in floating point. But FP numbers are weird:

    > 1e16 - 1 == 1e16
    true
At some point, they don't have enough precision to represent exact differences, so they start rounding, and this gets extremely more inaccurate as the value increases. For correct behavior, remainingDelay needs to be stored in BigInt.

Of course, this problem is mostly theoretical, as it starts happening at around 2^83 milliseconds, which doesn't even fit in a 64-bit time_t, and it's not like humanity will exist by then. But still!

paulddraper · 10 months ago
I would go so far as to say entirely theoretical.
rzz3 · 10 months ago
Awesome! Already have a project I can use this on, thanks.

As a side note, why do you use this weird non-Github, non-Gitlab, non-Bitbucket sketchy looking git host? I can see the code obviously, but it makes me worry about supply chain security.

malthejorgensen · 10 months ago
sourcehut isn’t weird at all.

It’s made by Drew Devault who is mostly well-respected in the hacker community, and it’s made exactly to be an alternative to BigCo-owned source hosts like GitHub, Gitlab and Bitbucket.

cchcch · 10 months ago
Drew isn't well-respected, he's been far too antagonistic to far too many people over the years for that.

Latest news is that he authored/published a controversial character assassination on Richard Stallman while trying and failing to stay anonymous. Then some further digging after this unmasking found he's into pedophilic anime. Sitting on his computer uploading drawings of scantily-clad children to NSFW subreddits.

No-one with any decency can respect that behavior, it's disgusting.