Skip to content

Quadratic time in scheme-less domain scan on crafted plain text #115

Description

@hey-jj

With url_must_have_scheme(false), LinkFinder::links runs in time quadratic to the input length on certain plain-text inputs. Every . becomes a trigger, so for each dot the domain scanner runs. find_domain_start walks back only to the nearest label boundary, but find_authority_end then walks forward across the whole following run of authority characters. A character such as ~ sets maybe_host = false and does not break the forward scan, so the scan reads to end of input and then fails, which leaves rewind unmoved, and the next dot repeats the same full forward walk. Tested against linkify 0.11.0.

An input built from the repeating unit a.a~ shows 4x growth in wall-clock time per doubling of input size, measured on a --release build:

    bytes        links         ms      ratio
    49152            0     417.06        -
    98304            0    1703.13     4.08
   196608            0    6711.25     3.94
   393216            0   27017.38     4.03

A 192 KB input already takes about 6.7 seconds. Plain prose, a plain letter run, and a dotted chain such as a.a. scale linearly on the same harness (about 2x per doubling) and finish in well under a millisecond at these sizes.

Reproducer

Cargo.toml:

[dependencies]
linkify = "0.11.0"

src/main.rs, run with cargo run --release:

use linkify::LinkFinder;
use std::time::Instant;

fn main() {
    let mut finder = LinkFinder::new();
    finder.url_must_have_scheme(false);
    for k in [48usize, 96, 192, 384] {
        let n = k * 1024;
        let unit = "a.a~";
        let mut input = unit.repeat(n / unit.len() + 1);
        input.truncate(n);
        let t = Instant::now();
        let count = finder.links(&input).count();
        println!("{:>7} bytes  {:>3} links  {:>9.2} ms", n, count, t.elapsed().as_secs_f64() * 1000.0);
    }
}

Scope

An application that turns on the scheme-less domain mode and feeds it text from an outside source can be pushed into seconds of CPU per request with an input under 200 KB, and worse with a larger one. The growth is quadratic, so a small increase in input size produces a large increase in processing time. That gives a cheap denial-of-service lever against any service scanning user-supplied text in this configuration.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions