Skip to content

Commit

Permalink
Merge pull request #19 from RuiSiang/waf-dev
Browse files Browse the repository at this point in the history
Waf dev
  • Loading branch information
RuiSiang authored Mar 23, 2021
2 parents 8e37b81 + 09b8e89 commit ab28be9
Show file tree
Hide file tree
Showing 32 changed files with 1,053 additions and 92 deletions.
8 changes: 6 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
PORT=3000
SESSION_KEY="abcdefghijklmnop"
WAF=on
POW=on
NONCE_VALIDITY=60000
INITIAL_DIFFICULTY=13
Expand All @@ -11,4 +10,9 @@ RATE_LIMIT_SAMPLE_MINUTES=60
RATE_LIMIT_SESSION_THRESHOLD=100
RATE_LIMIT_BAN_IP=on
RATE_LIMIT_IP_THRESHOLD=500
RATE_LIMIT_BAN_MINUTES=15
RATE_LIMIT_BAN_MINUTES=15

WAF=on
WAF_URL_EXCLUDE_RULES=
WAF_HEADER_EXCLUDE_RULES=14,33,80,96,100
WAF_BODY_EXCLUDE_RULES=
1 change: 0 additions & 1 deletion .github/workflows/njsscan-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ on:
jobs:
njsscan:
runs-on: ubuntu-latest
name: njsscan code scanning
steps:
- name: Checkout the code
uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/nodejs-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:

strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
node-version: [12.x, 14.x]

steps:
- uses: actions/checkout@v2
Expand Down
5 changes: 4 additions & 1 deletion CONFIGURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,7 @@ Ratelimit Options

WAF Options

- WAF: (default:on) toggles waf functionality on/off (waf is still a work in progress)
- WAF: (default:on) toggles waf functionality on/off
- WAF_URL_EXCLUDE_RULES: exclude rules to check when scanning request url, use ',' to seperate rule numbers, use '-' to specify a range (eg: 1,2-4,5,7-10)
- WAF_HEADER_EXCLUDE_RULES: (default:14,33,80,96,100) exclude rules to check when scanning request header, use ',' to seperate rule numbers, use '-' to specify a range (eg: 1,2-4,5,7-10)
- WAF_BODY_EXCLUDE_RULES: exclude rules to check when scanning request body, use ',' to seperate rule numbers, use '-' to specify a range (eg: 1,2-4,5,7-10)
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ nodejs and docker

envitonment variables

## References
+ Proof-of-work by Fedor Indutny (PoW utility functions)
+ Shadowd by Zesecure (WAF rules)

## TODOs

- [x] Web Service Structure
Expand All @@ -48,7 +52,7 @@ envitonment variables
- [x] IP Blacklisting
- [x] Ratelimiting
- [x] Unit Testing
- [ ] WAF Implementation
- [x] WAF Implementation
- [ ] Dynamic Difficulty
- [ ] Multi-Instance Syncing
- [ ] Monitoring
Expand Down
2 changes: 1 addition & 1 deletion app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { createProxyMiddleware } from 'http-proxy-middleware'
import c2k from 'koa2-connect'
import session from 'koa-session-minimal'

import config from './service/config-parser'
import config from './service/util/config-parser'
import powRouter from './routes/pow-router'
import testRouter from './routes/test-router'
import { controller } from './service/controller-service'
Expand Down
1 change: 0 additions & 1 deletion lib/bundle.js

This file was deleted.

10 changes: 0 additions & 10 deletions lib/pow.ts

This file was deleted.

9 changes: 2 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
"koa-static": "^5.0.0",
"koa-views": "^7.0.1",
"koa2-connect": "^1.0.2",
"minimalistic-assert": "^1.0.1",
"moment": "^2.29.1",
"pug": "^3.0.1",
"randomstring": "^1.1.5",
Expand All @@ -42,7 +41,6 @@
"@types/koa-router": "^7.4.1",
"@types/koa-session-minimal": "^3.0.5",
"@types/koa-views": "^2.0.4",
"@types/minimalistic-assert": "^1.0.1",
"@types/randomstring": "^1.1.6",
"@types/sqlite3": "^3.1.7",
"awesome-typescript-loader": "^5.2.1",
Expand Down
1 change: 1 addition & 0 deletions public/stylesheets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ a {
height: 100vh;
width: fit-content;
text-align: center;
text-align: -moz-center;
justify-content: center;
margin: 0 auto;
padding-top: 30vh;
Expand Down
2 changes: 1 addition & 1 deletion routes/pow-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Koa from 'koa'
import Router from 'koa-router'

import Pow from '../service/pow-service'
import config from '../service/config-parser'
import config from '../service/util/config-parser'

const router = new Router()
const pow = new Pow(config.initial_difficulty)
Expand Down
4 changes: 3 additions & 1 deletion scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ echo "Build Successful!" && \
echo "Copying Files..." && \
cp -rfRT views dist/views && \
cp -rfRT public dist/public && \
cp wafRules.json dist/wafRules.json && \
cp wafTypes.json dist/wafTypes.json && \
cp package.json dist/package.json && \
cp .env dist/.env && \
browserify lib/bundle.js -t uglifyify | uglifyjs > dist/public/javascripts/bundle.min.js && \
browserify service/bundle.js -t uglifyify | uglifyjs > dist/public/javascripts/bundle.min.js && \
echo "Success!"
1 change: 1 addition & 0 deletions service/bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
window.powSolver = require('../dist/service/pow/solver.js').Solver;
46 changes: 29 additions & 17 deletions service/controller-service.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,44 @@
import Koa from 'koa'
import Blacklist from './controllers/blacklist'
import config from './config-parser'
import config from './util/config-parser'
import Ratelimiter from './controllers/rate-limiter'
import Waf from './controllers/waf'

export const controller: Koa.Middleware = async function (
ctx: Koa.ParameterizedContext,
next: Koa.Next
) {
const blacklist = Blacklist.getInstance()
const rateLimiter = Ratelimiter.getInstance()
if ((await blacklist.check(ctx.ip)) || !config.rate_limit) {
if (!!ctx.session.authorized || !config.pow) {
if (config.rate_limit) {
await Object.assign(
ctx.session,
await rateLimiter.process(ctx.ip, ctx.session)
)
}
next()
} else {
console.log(ctx.request.url)
if (ctx.request.url == '/') {
ctx.redirect('/pow')
} else if (ctx.request.url == '/pow') {
await next()
const waf = Waf.getInstance()
if (await blacklist.check(ctx.ip)) {
const scanResult = await waf.scan(ctx)
if (!scanResult) {
if (!!ctx.session.authorized || !config.pow) {
if (config.rate_limit) {
await Object.assign(
ctx.session,
await rateLimiter.process(ctx.ip, ctx.session)
)
}
next()
} else {
ctx.redirect(`/pow?redirect=${ctx.request.url}`)
if (ctx.request.url == '/') {
ctx.redirect('/pow')
} else if (ctx.request.url == '/pow') {
await next()
} else {
ctx.redirect(`/pow?redirect=${ctx.request.url}`)
}
}
} else {
console.log(
`Rule ${scanResult.id}: "${scanResult.cmt}" in category "${
scanResult.type
}" has been triggered by request ${scanResult.location} at ${new Date().toISOString()}`
)
ctx.status = 403
await ctx.render('waf')
}
} else {
ctx.status = 403
Expand Down
5 changes: 3 additions & 2 deletions service/controllers/blacklist.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CronJob } from 'cron'
import Database from '../database-service'
import Database from '../util/database-service'
import config from '../util/config-parser'

class Blacklist {
private static instance: Blacklist
Expand All @@ -22,7 +23,7 @@ class Blacklist {
if (!dbQuery.length) {
return true
}
return false
return false || !config.rate_limit
}
public ban = async (ip: string, minutes: number) => {
await this.db.queryAsync({
Expand Down
4 changes: 2 additions & 2 deletions service/controllers/rate-limiter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import moment from 'moment'
import { CronJob } from 'cron'
import Database from '../database-service'
import Database from '../util/database-service'
import Blacklist from './blacklist'
import config from '../config-parser'
import config from '../util/config-parser'

class RateLimiter {
private static instance: RateLimiter
Expand Down
146 changes: 146 additions & 0 deletions service/controllers/waf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import fs from 'fs'
import path from 'path'
import config from '../util/config-parser'
import { ParameterizedContext } from 'koa'

interface Rule {
reg: RegExp
type: number
cmt: string
}

interface _Rule {
id: number
reg: string
type: number
cmt: string
}

class Waf {
private static instance: Waf
public static getInstance(): Waf {
if (!Waf.instance) {
Waf.instance = new Waf()
}
return Waf.instance
}

constructor() {
this.load()
}

private types: {
[key: string]: string
} = {}

private rules: {
[key: string]: Rule
} = {}

private entries: { [key: string]: RegExp } = {}

private load = () => {
this.types = JSON.parse(
fs.readFileSync(path.join(process.cwd(), 'wafTypes.json')).toString()
)
const rulesJson = JSON.parse(
fs.readFileSync(path.join(process.cwd(), 'wafRules.json')).toString()
)

for (let key in this.types) {
this.types[this.types[key]] = key
}
rulesJson.forEach((rule: _Rule) => {
this.rules[rule.id] = {
reg: new RegExp(rule.reg),
type: rule.type,
cmt: rule.cmt,
}
this.entries[rule.id] = new RegExp(rule.reg)
})
}

private parseNumString = (numString: string) => {
const substrings = numString.split(',')
let numArr: number[] = []
substrings.forEach(function (item) {
if (!!item) {
const tmpArr = item.split('-')
if (tmpArr.length == 2) {
const low = parseInt(tmpArr[0])
const high = parseInt(tmpArr[1])
for (let i = low; i <= high; i++) {
numArr.push(i)
}
} else {
numArr.push(parseInt(tmpArr[0]))
}
}
})
return numArr
}

private detect = async (test: string, excludes: number[]) => {
for (let key in this.entries) {
if (!excludes.includes(parseInt(key))) {
if (this.entries[key].test(test) === true) {
return key
}
}
}
return 0
}

public scan = async (ctx: ParameterizedContext) => {
if (!!config.waf) {
const urlExcludeRules = this.parseNumString(config.waf_url_exclude_rules)
const urlResult = await this.detect(ctx.url, urlExcludeRules)
if (!!urlResult) {
return {
id: urlResult,
type: this.types[this.rules[urlResult].type],
cmt: this.rules[urlResult].cmt,
location: 'url',
}
}
const headerExcludeRules = this.parseNumString(
config.waf_header_exclude_rules
)
const headerResult = await this.detect(
JSON.stringify(ctx.headers),
headerExcludeRules
)
if (!!headerResult) {
return {
id: headerResult,
type: this.types[this.rules[headerResult].type],
cmt: this.rules[headerResult].cmt,
location: 'header',
}
}
const bodyExcludeRules = this.parseNumString(
config.waf_body_exclude_rules
)
const bodyResult = await this.detect(
JSON.stringify(ctx.request.body),
bodyExcludeRules
)
if (!!bodyResult) {
return {
id: bodyResult,
type: this.types[this.rules[bodyResult].type],
cmt: this.rules[bodyResult].cmt,
location: 'body',
}
}
}
return null
}
public test = async (test: string, excludes: number[]) => {
if (process.env.NODE_ENV === 'test') {
return await this.detect(test, excludes)
}
return 0
}
}
export default Waf
Loading

0 comments on commit ab28be9

Please sign in to comment.