目前大多數應用程序都會包含:服務器端邏輯、客戶端邏輯、數據存儲、數據傳輸、以及API等多個組件。與此同時,每種語言、框架、以及環境的使用,都會讓應用程序暴露于一組獨特的漏洞之中。為了保證這些組件的安全,第一時間發現應用的漏洞,進而構建出一個安全態勢較高的應用,往往需要我們付出不懈的努力。
值得慶幸的是,大多應用程序的漏洞都有著相似、甚至相同的底層原因。研究這些常見的漏洞類型、以及背后的原因,將有助于我們對應用程序進行恰當的防護。
下面,我將和您一起討論影響Angular和React應用的如下六種最常見的漏洞,以及如何發現和預防它們:
- 身份驗證繞過
- 訪問控制不當
- 開放式重定向
- 跨站請求偽造 (CSRF)
- 模板注入
- 跨站點腳本包含 (XSSI)
身份驗證繞過
身份驗證是指在執行敏感操作、或訪問敏感數據之前,先驗明身份的合法性。如果在應用程序上未能正確地實施身份驗證,那么攻擊者將可以利用此類錯誤配置,來訪問他們本不該能夠訪問到的服務與功能。例如,Angular通常使用AppRoutingModule來進行路由。在將用戶引導至應用程序中的敏感路由之前,您應該檢查用戶是否已通過了身份驗證,并被授權了訪問該資源。請參考如下代碼段:
({
imports: [RouterModule.forRoot([
// These paths are available to all users.
{ path: '', component: HomeComponent },
{ path: 'features', component: FeaturesComponent },
{ path: 'login', component: LoginComponent },
// These routes are only available to users after logging in.
{ path: 'feed', component: FeedComponent, canActivate: [ AuthGuard ]},
{ path: 'profile', component: ProfileComponent, canActivate: [ AuthGuard ]},
// This is the fall-through component when the route is not recognized.
{ path: '**', component: PageNotFoundComponent}
])],
exports: [RouterModule]
})
export class AppRoutingModule {}
訪問控制不當
攻擊者會想方設法繞過那些訪問權限控制實施不當的應用程序。訪問控制不僅僅只包括身份驗證。也就是說,我們除了需要證明用戶的身份(即“你是誰?”),還要通過應用程序授予相應的權限(即“你可以做什么?”)。只有通過兩者雙管齊下,才能共同確保用戶不會訪問到超出其權限的服務與功能。
目前,我們有多種方式為用戶配置授權,其中包括:基于角色的訪問控制、基于所有權的訪問控制、以及訪問控制列表等。而開發人員常犯的一種錯誤是在客戶端執行授權檢查。由于攻擊者可以操控和覆蓋客戶端的檢查,因此這是不安全的。可見,此類授權檢查必須使用服務器端的代碼來執行。請參考如下代碼段:
export class AdministratorGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<true | UrlTree> {
// Check whether this user is an administratoor.
return this.authService.isAdmin().pipe(
map(isAdmin => {
if (!isAdmin) {
return this.router.parseUrl('/')
}
return isAdmin
})
)
}
}
export class AuthService {
constructor(private http: HttpClient) {}
// Whether the user is currently logged in.
loggedIn: boolean | null = null
// The user object object encompassing the user's name and role. Will be set
user: User | null = null
// Check whether the current user is an administrator.
isAdmin(): Observable<boolean> {
return this.getCurrentUser().pipe(map(user => {
return user != null && user.role === 'admin'
}))
}
// Get the user definition from local state, or the server (if this is the first time we are checking).
getCurrentUser(): Observable<User | null> {
if (this.loggedIn !== null) {
return of(this.user)
}
return this.http.get<User>('/api/auth', {
responseType: 'json'
}).pipe(
tap({
next: user => {
// If we get a user definition from the server it indicates this user is logged in.
this.user = user
this.loggedIn = true
},
error: error => {
// A 401 response from the server indicates this user is not logged in.
this.user = null
this.loggedIn = false
}
}),
catchError(() => {
return of(null)
})
)
}
}
export interface User {
username: string
role: string
}
開放式重定向
例如,當未經身份驗證的用戶嘗試著訪問需要登錄后才能看到的頁面時,網站就需要將該用戶自動重定向到登錄頁面,并在他們通過了身份驗證之后,再讓其返回到原來的位置。
在開放式重定向攻擊發生時,攻擊者通過向用戶提供來自合法站點的URL,以欺騙用戶訪問某個外部站點。也就是說,該URL會將其重定到其他站點。而該站點一面設法讓用戶相信他們仍然在原始網站上,一面幫助攻擊者構建出更加看似可信的網絡釣魚活動。
為了防止開放式重定向,您需要確保應用程序不會輕易將用戶重定向到那些惡意站點的位置。例如,您可以通過驗證重定向URL,來完全禁止離站重定向行為。請參考如下代碼段:
export class LoginComponent {
// The username and password entered by the user in the login form.
username = '';
password = '';
// The destination URL to redirect the user to once they log in successfully.
destinationURL = '/feed'
constructor(private authService : AuthService,
private route : ActivatedRoute,
private router : Router) { }
ngOnInit() {
this.destinationURL = this.route.snapshot.queryParams['destination'] || '/feed';
}
onSubmit() {
this.authService.login(this.username, this.password)
.subscribe(
() => {
// After the user has lgged in, redirect them to their desired destination.
let url = this.destinationURL
// Confirm that the URL is a relative path - i.e. starting with a single '/' characters.
if (!url.match(/^\/[^\/\\]/)) {
url = '/feed'
}
this.router.navigate([ url ])
})
}
}
當然,我們還有許多其他方法可以防止開放式重定向的發生。例如:對請求引用方予以檢查、或使用頁面索引進行重定向。不過,正因為驗證URL相對比較困難,因此開放式重定向仍然是當代Web應用普遍存在的問題。
跨站請求偽造
跨站點請求偽造(Cross-Site Request Forgery,CSRF)是一種客戶端技術,可用于攻擊Web應用的其他用戶。使用CSRF,攻擊者可以發送虛假的、來自受害者的HTTP請求,去執行攻擊者的危害性操作。例如,攻擊者會在未經許可的情況下,更改受害者的密碼、或從其銀行賬戶里轉賬。
與開放式重定向不同,我們目前已有一種行之有效的對抗CSRF的方法,即:使用CSRF令牌和SameSite Cookie的組合,以避免使用GET請求進行各項狀態更改的操作。例如,Angular允許您使用HttpClientXsrfModule模塊,向HTTP請求添加防偽的令牌。請參考如下代碼段:
({
declarations: [],
imports: [
BrowserModule,
HttpClientModule,
HttpClientXsrfModule.withOptions({
cookieName: 'XSRF-TOKEN',
headerName: 'X-CSRF-TOKEN'
}),
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
}
模板注入
類似于HTML文件的Web模板,為開發人員提供了一種通過將應用數據與靜態模板相結合,以指定如何呈現頁面的方法。此功能允許開發人員將從數據庫或HTTP請求中檢索到的動態內容,插入到網頁中。
顧名思義,模板注入需要注入到網頁的模板中。根據受感染應用的權限,攻擊者可以通過使用模板注入的漏洞,來讀取敏感文件、執行惡意代碼、或提升他們在系統上的各種權限。下面展示了Angular模板的不安全用法。它允許攻擊者通過URL的哈希值,來惡意注入代碼:
({
selector: 'app-header',
template: '<h1>' + (window.location.hash || 'Home') + '</h1>'
})
export class HeaderComponent {}
注意:請千萬不要直接將用戶提供的輸入連接到模板中,而應該使用模板引擎的內置替換機制,來安全地嵌入動態輸入。請參考如下代碼段:
({
selector: 'app-header',
template: '<h1>{{ title }}</h1>'
})
export class HeaderComponent {
title = ''
ngOnInit() {
this.title = window.location.hash || 'Home';
}
}
跨站點腳本
跨站點腳本包含的攻擊也稱為XSSI(Cross-Site Script Inclusion)。此類攻擊發生在當惡意站點包含了來自受害者站點的Javascript,并通過腳本提取其敏感信息時。
同源策略(same-origin policy,SOP)通常可以起到控制數據跨源(cross-origins)訪問的作用。不過,SOP并不能限制JavaScript代碼,而且HTML<script>標簽會允許從任何來源加載JavaScript代碼。從技術角度來說,該功能方便了允許跨域重用的JavaScript文件,但是會帶來新的安全風險:攻擊者可以通過加載受害者的JS文件,來竊取寫入JavaScript文件的數據。
例如,某網站通過Javascript文件,為登錄用戶存儲和傳輸敏感數據。如果用戶在同一瀏覽器中訪問了惡意站點,那么惡意站點可以導入該JavaScript文件,并訪問與該用戶會話相關的敏感信息。而這一切都歸因于存儲在瀏覽器中的用戶Cookie。
因此,為避免XSSI攻擊,請勿在JavaScript文件中傳輸敏感數據。下面是如何使用JSON文件(會受到SOP的限制)在Angular中安全地加載API令牌的示例:
// The configuration information we will retrieve from the server.
export interface Config {
username : string
accessToken : string
role : string
}
()
export class ConfigService {
constructor(private http: HttpClient) {}
// Retrieve configuration information from the server.
getConfig() {
return this.http.get<Config>('api/config')
.pipe(
catchError(this.handleError)
)
}
private handleError(error: HttpErrorResponse) {
if (error.status === 0) {
// A client-side or network error occurred. Handle it accordingly.
log.error('An error occurred:', error.error)
} else {
// The server returned an unsuccessful response code.
log.error(`Backend returned code ${error.status}, body was: `, error.error)
}
return throwError('An unexpected error occurred loading configuration.')
}
}
譯者介紹
陳峻 (Julian Chen),51CTO社區編輯,具有十多年的IT項目實施經驗,善于對內外部資源與風險實施管控,專注傳播網絡與信息安全知識與經驗;持續以博文、專題和譯文等形式,分享前沿技術與新知;經常以線上、線下等方式,開展信息安全類培訓與授課。
原文標題:??Angular + React: Vulnerability Cheatsheet??,作者: Vickie Li