Hva med sikkerhet?
Akkurat nå er app’en vår 100% sikker, fordi den kun lever på vår egen datamaskin – det er ingen andre som kan komme inn på app’en. Men, hvis vi har planer om å noen gang laste opp app’en vår til en server, slik at verden får kontakt med den, så bør vi gjøre noen få grep for å implementere grunnleggende sikkerhet.
I server.js, gå til linja der du finner GET-endepunktet for rotområdet, altså ‘/’ (app.get(‘/’, …). Lag deg noen linjeskift der, og lang en funksjon. Som vanlig spiller ikke navnet på funksjonen noen rolle, men la oss kalle den passwordProtected
function passwordProtected() {
}
La oss legge til tre parameter i mellom parantesene: req, res og next.
I funksjonskroppen kan vi bare logge ut til konsollet så lenge:
console.log('Our custom function just ran')
Og faktisk, la oss legge til et kall til next-metoden på linja under:
next()
I express – rammeverket vi bruker for å lage en webapplikasjon med Node – finnes det metoder vi har brukt hele tiden som heter f.eks app.post() eller app.get(). Som det første argumentet i disse funksjonene har vi sendt inn en adresse (f.eks ‘/’, ‘/create-item’, ‘/delete-item’), og som det andre argumentet har vi sendt inn en funksjon som blir kalt når adressen blir forespurt (requested). Det vi ikke har gjort hittil er å inkludere flere funksjoner som argument. Og det kan man, gitt at alle funksjonene (minus den siste) kaller next(), når den er ferdig med å gjøre det den skal. next()-metoden gjør altså at vi kaller den neste funksjonen i rekka.
La oss snike inn passwordProtected som et argument i app.get(‘/’, …), mellom argument 1 og 2:
app.get('/', passwordProtected, function(req, res) {
// ...
})
Altså, så lenge passwordProtected ikke støter på noen problemer, og får kjørt next(), så vil nettsiden vises som vanlig. Lagre fila, og oppdater nettleseren. Sjekk terminalvinduet, og se at du får opp “our custom function just ran”, og legg merke til at nettsiden fortsatt blir lastet inn.
Nå kan vi gå tilbake til passwordProtected-metoden, og bytte ut hele console.log-linja med:
res.set('WWW-Authenticate', 'Basic realm="Simple Todo App"')
Så… hva foregår her? HTTP, som er en protokoll vi bruker når vi er på internet (http://www.nettadresse.com) inneholder mange forskjellige egenskaper som vi ikke trenger å forholde oss veldig til. Men her trenger vi å sette et autentiserings-attributt (argument a), og deretter fortelle den hva app’en vår heter (argument b).
Fjern next-metoden, og sett inn en if-else block:
if () {
} else {
}
I parantesene til if’en må vi sjekke om authorization-headeren (igjen, et HTTP-attributt), samsvarer med noe vi bestemmer. For en test kan vi sjekke at den er lik strengen Placeholder:
if (req.headers.authorization == "Placeholder") {
Inni if-blocka kan du legge inn next-metoden. I else-blokka kan vi bestemme hva som skjer dersom brukeren ikke har tastet rett passord. Vi kan da sende en HTTP-respons som heter “Unauthorized”. HTTP-responser har flere tallkoder, og unauthorized er tallkode 401. Den mer vanlige som vi møter på rundt om kring på internett er gjerne 404 – File not found. La oss sende 401 til brukeren, med en melding i else-blokka:
} else {
res.status(401).send("Authentication required")
Rett over denne blokka kan vi logge ut hva brukeren tastet inn som brukernavn og passord, bare for testing:
console.log(req.headers.authorization)
Til sammen har vi da:
function passwordProtected() {
res.set('WWW-authenticate', 'Basic realm="Simple Todo App"')
console.log(req.headers.authorization)
if (req.headers.authorization == 'Placeholder') {
next()
} else {
res.status(401).send("Authentication required")
}
}
IT-sikkerhet er et gedigent tema, som vi ikke skal herje altfor mye med, men hvis du er usikker på hva de to lange ordene på a er skal jeg gjøre et forsøk på å forklare forskjellen her:
Autentisering: Bekrefting på at du er den du sier at du er (f.eks brukernavn og passord)
Autorisering: Når du er autentisert, hvilke tilganger har du?
Nå kan vi lagre fila og laste inn nettsida på nytt. Du vil da få opp et nytt passord-felt. I koden ber vi om å sjekke etter “Placeholder”, som ikke gir helt mening siden vi har to felt – hvilke av dem sjekkes? Prøv å bare skriv inn noe helt tilfeldig (f.eks asdfghjkqwerzcv), både til passord og brukernavn. Hvis du går tilbake til terminalen kan vi se hva som ble logget ut. Det vil stå Basic, og så en lang streng med tekst. Det er fordi teksten har blitt kryptert, med noe som heter Base-64 format.
Vi kan bruke dette til å lage en sikker løsning, hvor vi setter opp ett sett med fungerende brukernavn og passord. Gå tilbake til nettleseren, og prøv å skriv inn det du vil ha som brukernavn og passord, f.eks guest som brukernavn, og kjempesterktpassord som passord. Uansett hva du velger som passord kan du sjekke konsollet igjen. Vi har fått en Base-64 streng til. Kopier hele strengen inkludert ordet Base, og bytt ut ordet Placeholder i koden vår, med det. Herved, hvis du prøver å logge inn med brukernavnet og passordet som du har valgt, så vil du komme inn på sida. Neat!
Du kan teste dette ut ved å starte et privat / incognito-vindu i nettleseren for å teste dette ut, siden et slikt vindu ikke husker passord som du skriver inn. Hvis brukeren trykker cancel, vil de få se “Authentication Required”-meldinga som vi lagde.
Nå er forsiden vår sikret, men vi har jo andre endepunkt enn bare rot-området (‘/’), som vi ønsker å sikre. Vi ønsker ikke at noen uten brukernavn og passord skal kunne slette ting i databasen vår med POST-endepunktet vårt, delete-item f.eks.
For å løse dette er kanskje den første tanken at vi bare kopierer inn passwordProtected-funksjonen vår i alle endepunktene, slik som vi gjorde for rot-området / . Nå har ikke vi et utall av endepunkt, men hvis applikasjonen vår hadde vært større, og hatt opp mot 50 endepunkt, ville det fort blitt repetativt og vanskelig å håndtere noe slikt. Siden vi vet at vi ønsker sikkerhet på alle endepunkt kan vi heller gjøre følgende:
Begynn med å fjerne passwordProtected som argument fra app.get(‘/’, …).
Rett over den linja legger vi inn følgende:
app.use(passwordProtected)
Det forteller express at den skal bruke den funksjonen for alle endepunktene vi har.
Vi deler kun brukernavn og passord med venner og bekjente som vi stoler på, men likevel finnes det folk som klarer å komme seg igjennom en slik type sikring.
Hvis vi har et input-felt som skriver ut html, så finnes det måter å misbruke et slikt felt, til å skrive ut script-tag’er, og deretter gå bananas med javascript-kode i input feltet, som potensielt kan være skadelig for siden vår. For å unngå dette kan vi installere en pakke som heter sanitize-html. Stopp serveren i terminalen (ctrl+c), og skriv inn
$ npm install sanitize-html
Deretter kan vi gå helt til toppen av server.js, og importere den pakken, f.eks rett under mongodb:
let sanitizeHTML = require('sanitize-html')
Med denne pakken installert kan vi gå til post-endepunktet vårt for /create-item. Det potensielt farlige feltet vi har, er der vi lar brukeren skrive inn tekst. Dropp db.collection-linja ned et felt, og legg inn en ny linje med kode der:
let safeText = sanitizeHTML(req.body.text, b)
Vi bruker altså den nye pakken vi har installert, og som første argument sender vi inn hvilken tekst som skal renses (saniteres). Som andre argument tar den inn en del innstillinger for hvordan den skal rense teksten, og det tar den inn som et javascript object:
{allowedTags: [], allowedAttributes: []}
Vi sender inn tomme arrays til både allowedTags og allowedAttributes, for å definere at vi ikke ønsker at brukeren skal kunne benytte HTML-tags (f.eks <script></script>) eller HTML-attributter (id=”example”). Linja i helhet blir altså:
let safeText = sanitizeHTML(req.body.text, {
allowedTags: [],
allowedAttributes: []
})
Vi kan gå ned til insertOne-funksjonen, og bytte ut req.body.text med safeText:
.insertOne({text: safeText}, ...)
Vi har et post-endepunkt for update-item, der vi er utsatt for samme type angrep, så la oss gjøre det samme der.