Sikkerhed

    Sådan virker hemmelige engangslinks

    Et hemmeligt engangslink viser indholdet én gang: browseren krypterer det, serveren gemmer kun chifferteksten, og første visning sletter den gemte kopi.

    W. Miller · Opdateret · Om LinkPilot

    Et hemmeligt engangslink er en URL, der viser sit indhold præcis én gang og derefter holder op med at virke for altid. Afsenderens browser krypterer hemmeligheden med AES-GCM-256, serveren gemmer kun chifferteksten, og nøglen ligger i den del af linket, der står efter #k=, som browsere aldrig sender. Når modtageren åbner indholdet første gang, dekrypteres det, og den gemte kopi slettes i samme operation.

    Du kan lave et uden konto med den gratis generator til hemmelige links.

    Selve mekanismen er enkel. Det interessante er grænsetilfældene, for det er dér, sjuskede implementeringer lækker, uden at nogen opdager det.

    Det grundlæggende forløb

    1. Afsenderen indsætter en hemmelighed. Browseren genererer en tilfældig nøgle, krypterer indholdet med AES-GCM-256 via Web Crypto API og sender kun chifferteksten. Serveren gemmer den under en tilfældig slug, der er svær at gætte, og returnerer en URL. Browseren tilføjer nøglen som et #k=-fragment, der aldrig forlader afsenderens enhed.
    2. Afsenderen deler URL'en via en hvilken som helst kanal: e-mail, chat, sms.
    3. Modtageren åbner URL'en. Et almindeligt GET returnerer kun status. Når modtageren klikker på Vis, markerer serveren hemmeligheden som brugt og returnerer chifferteksten i ét og samme trin, der ikke kan deles op. Modtagerens browser dekrypterer den med nøglen fra fragmentet.
    4. Alle senere besøg får en statusside om, at indholdet er slettet, i stedet for indholdet.

    Det afgørende trin er trin 3. Sker læsningen og statusopdateringen som to separate forespørgsler, først SELECT for at tjekke og så UPDATE for at markere, kan to samtidige besøgende begge komme igennem tjekket, før nogen af dem skriver. Begge får indholdet. Det er en klassisk TOCTOU-fejl (time-of-check to time-of-use), og den gør garantien om ”præcis én gang” til ”for det meste én gang”.

    Hvorfor skal sletningen og læsningen ske i ét trin?

    Fordi to samtidige besøgende ellers begge kan komme igennem tjekket, før nogen af dem markerer hemmeligheden som brugt. Løsningen er at samle tjekket, markeringen og læsningen i én databaseoperation. I Postgres ser det sådan ud med UPDATE ... RETURNING:

    UPDATE secrets
    SET burned_at = now()
    WHERE slug = $1
      AND burned_at IS NULL
      AND expires_at > now()
    RETURNING payload;
    

    Den rækkelås, som UPDATE tager, betyder, at samtidige kald bliver afviklet ét ad gangen: Det første matcher WHERE-betingelsen, sætter burned_at og får indholdet tilbage via RETURNING. Alle senere kald finder burned_at allerede sat, matcher nul rækker og får status som slettet. Der er intet vindue mellem ”tjek” og ”forbrug”, fordi de er den samme sætning.

    Netop derfor registrerer LinkPilot visninger i ét udeleligt trin gennem en databasefunktion: Samtidige forsøg på at vise hemmeligheden kan ikke lække den to gange. Vurderer du et værktøj, er det et rimeligt spørgsmål at stille leverandøren direkte: sker sletningen og læsningen i ét trin, der ikke kan deles op?

    Fordi 410 Gone siger, at ressourcen har eksisteret og er permanent utilgængelig, og det er præcis, hvad en slettet hemmelighed er. Når linket er brugt, skal det fejle tydeligt og permanent. LinkPilot gør to ting, når indholdet slettes:

    • Det gemte indhold bliver overskrevet med [REDACTED] i den samme skrivning, som ændrer status. Selv hvis databasen senere bliver kompromitteret, kan en brugt hemmelighed ikke gendannes.
    • Senere forsøg på at vise indholdet returnerer HTTP 410 Gone med en tydelig side om, at indholdet ”allerede er vist og destrueret”.

    410 er den rigtige statuskode: Den betyder, at ressourcen har eksisteret og med vilje er permanent utilgængelig, i modsætning til 404, som siger ”aldrig hørt om den”. Forskellen betyder også noget for mennesker. En modtager, der ser ”denne hemmelighed er allerede vist”, ved, at afsenderen skal have besked. Afsenderen kan så tjekke revisionsloggen og skifte adgangskoden, hvis det ikke var modtageren, der åbnede den.

    Slugs, der er svære at gætte, og hvorfor de ikke er nok

    Sluggen er det eneste, der står mellem det offentlige internet og en ulæst chiffertekst (nøglen ligger i fragmentet, ikke på serveren), så den skal have reel entropi. En slug, der er trukket fra en kryptografisk sikker tilfældighedskilde med fx 22 tegn i base62, har omkring 128 bit. Det er ikke et realistisk angreb at gennemløbe dem ved nogen forespørgselsrate, en server ville overleve.

    Men entropien i sluggen beskytter kun mod at blive gættet. Den gør intet ved, at URL'en bliver set: browserhistorik, proxylogfiler, videresendte e-mails, skærmdeling, udklipsholderprogrammer. Derfor bygger seriøse implementeringer flere lag ovenpå:

    • Udløb, så en URL, der er blevet set, holder op med at virke til tiden.
    • En adgangssætning, så en URL, der er blevet set, ikke er nok i sig selv.
    • Sletning efter læsning, så en URL, der er blevet set, er værdiløs, når den rette modtager har læst indholdet.

    Chatklienter (Slack, Teams, iMessage) og sikkerhedsscannere til e-mail laver forhåndsvisninger af URL'er ved at hente dem med en GET-forespørgsel, i det øjeblik beskeden bliver leveret. Sletter dit endpoint til visning indholdet ved GET, sletter forhåndsvisningsbotten hemmeligheden, før et menneske overhovedet ser den. Det er den mest almindelige fejl i praksis hos naive værktøjer med sletning efter læsning.

    Sådan beskytter LinkPilot mod det:

    • Separate handlinger til metadata og visning. Et GET på visnings-endpointet returnerer kun status, og om der kræves en adgangssætning. En visning registreres kun ved et eksplicit POST, som er den handling, et menneske udfører ved at klikke på knappen Vis.
    • Adgangssætning (Pro). Når der er sat en adgangssætning, returneres indholdet ikke til nogen, hverken bot eller menneske, uden den korrekte hash.
    • Cache-Control: no-store på alle svar med indhold, så mellemled aldrig cacher hemmeligt indhold.

    LinkPilots side om sikkerhedsarkitektur dokumenterer håndteringen fra ende til anden.

    Livscyklussen, tilstand for tilstand

    TilstandUdløses afHvad en besøgende serKan indholdet gendannes?
    AktivOprettelseVisningsside (beder om adgangssætning, hvis den er sat)Ja, én visning er tilbage
    SlettetGrænsen for antal visninger er nået”Allerede vist og destrueret”, HTTP 410Nej, indholdet er overskrevet
    UdløbetUdløbstidspunktet passeresStatusside om udløbNej, overskrives ved næste adgang
    TilbagekaldtAfsenderen lukker det fra dashboardetViser ikke indholdet, HTTP 410Nej, med det samme og permanent

    Hver overgang havner i revisionsloggen, som er en Pro-funktion i LinkPilot. Hændelsestyperne er created, viewed, failed_passphrase, burned, expired og revoked, hver med et tidsstempel og en hashet IP-adresse (SHA-256 med et salt, der skiftes dagligt; den rå IP-adresse gemmes aldrig). Det spor gør ”jeg tror, de fik det” til ”vist kl. 10:05 fra det forventede land, én visning, derefter slettet”.

    Almindelige udvidelser

    • Adgangssætning: Kræv en adgangssætning, der sendes ad en anden kanal, før indholdet vises, så en lækket URL alene ikke er en lækket hemmelighed. I LinkPilot er det en Pro-funktion (29 $/md.) sammen med selvvalgt udløb og revisionsloggen; generatoren uden konto kan ikke sætte en. Begræns antallet af forsøg: LinkPilot tillader 5 pr. IP-adresse pr. minut pr. hemmelighed og logger hver fejl.
    • Tidsbaseret udløb: Hemmeligheden forsvinder efter et fastsat tidsrum, også selv om den aldrig er læst. LinkPilots formular for brugere, der er logget ind, tilbyder 1 time, 24 timer eller 7 dage på alle planer og en selvvalgt dato og tid på Pro. Generatoren uden konto sætter 7 dage som standard. Det dækker det tilfælde, hvor linket bliver glemt i en indbakke, som sletning efter læsning ikke kan klare.
    • Manuel tilbagekaldelse: Afsenderen kan lukke en ulæst hemmelighed fra dashboardet, så snart noget virker forkert. En billig forsikring, når man har sendt det til den forkerte Alex.

    Grænsetilfælde, der skiller gode implementeringer fra dårlige

    Den forkerte person åbner det først

    Sletning efter læsning forhindrer ikke den forkerte i at se indholdet, men den gør fejlleveringen synlig. Den rette modtager møder en side om, at indholdet er slettet, revisionsloggen viser en visning, du ikke ventede, og du skifter loginoplysningerne. Det er ubehageligt, men afgrænset: præcis én eksponering, opdaget hurtigt. Sammenlign med en hemmelighed, der er indsat i en chat, hvor du aldrig ville opdage det.

    Problemet med skærmbilleder

    Et engangslink giver én visning, ikke én kopi. Modtageren kan tage et skærmbillede af hemmeligheden, kopiere den eller skrive den af. Intet ved linket forhindrer det, og leverandører, der antyder noget andet, sælger fiktion. Den ærlige beskrivelse: Sletning efter læsning styrer leveringskanalen, ikke modtageren. Tillid til modtageren er en forudsætning, ikke et resultat.

    Prefetch og spekulativ indlæsning i browseren

    Nogle browsere og apps henter på forhånd de URL'er, man sandsynligvis vil klikke på. Fordi et velbygget værktøj kun sletter ved et eksplicit POST, opfører prefetch sig præcis som en forhåndsvisningsbot: Den kan hente selve siden, men aldrig indholdet.

    • Værdien skal bruges igen. At kunne hente noget frem igen er hele formålet med en adgangskodeadministrator og det modsatte af formålet med et engangslink. Brug et element i en boks.
    • Flere modtagere skal have den. Ét link, én læsning. Skal flere have den, må du enten oprette ét link pr. person (fint, og det kan spores) eller erkende, at du i stedet har brug for fælles adgangsstyring.
    • Det er en maskine, der skal bruge den. Tjenester og CI skal læse fra en secrets manager, ikke klikke på links.

    Vil du se mekanismen i aktion, så opret et med den gratis generator til hemmelige links: ingen konto, krypteret i din browser og væk efter første visning.

    Kilder

    Værktøjer til denne artikel

    Ofte stillede spørgsmål

    LinkPilot sender et hemmeligt link, der er væk, når det er åbnet.

    Krypteres i din browser, vises én gang og destrueres derefter. Kræver ingen konto.

    Opret et hemmeligt link

    Læs videre