OutSystems provides built-in XSS and SQL injection protection through Escape Content and parameterized Aggregates, but dangerous escape hatches like Expand Inline and Escape Content = No can introduce critical vulnerabilities. This tutorial covers auditing these settings, using EncodeSql() and EncodeHtml() correctly, configuring CSRF protection, implementing input validation, running AI Mentor Studio security scans, and applying HTTP security headers.
OutSystems Security: What's Automatic and What You Must Configure
OutSystems provides strong security defaults — Aggregates use parameterized queries, Expression widgets escape HTML by default, and Traditional Web apps have built-in CSRF tokens. However, these protections have escape hatches (literally: Expand Inline = Yes, Escape Content = No) that when misused create critical vulnerabilities. This guide covers the full security checklist: understanding built-in protections, the specific configurations that override them, manual encoding functions for edge cases, HTTPS and header security, and how to use AI Mentor Studio's taint analysis to find vulnerabilities programmatically across your codebase.
Prerequisites
- An OutSystems 11 or ODC module open in Service Studio
- Access to AI Mentor Studio (available in both O11 and ODC environments)
- Familiarity with Server Actions, Aggregates, and the SQL query element
- Basic understanding of web security concepts (XSS, SQL injection, CSRF)
Step-by-step guide
Audit Expression Widgets for Escape Content Setting
Audit Expression Widgets for Escape Content Setting
In Service Studio, open any screen in the **Interface tab**. Click **Find** (Ctrl+F) → search for `Escape Content` with value `No`. Review every result. The **Expression** widget's `Escape Content` property defaults to **Yes**, which automatically HTML-encodes output and prevents XSS. Setting it to No is only justified when you are intentionally rendering HTML markup from a trusted source. **When to use Escape Content = No (rarely):** - Rendering sanitized HTML from a CMS that has already stripped dangerous tags - Displaying formatted rich text stored in your database that was sanitized on input **When it is dangerous:** - Displaying any user-supplied text input - Rendering values from external APIs - Showing database content that was not sanitized on write For every Escape Content = No instance, either change it back to Yes or add explicit `EncodeHtml()` wrapping for the unsafe parts.
1/* SAFE: Expression widget with Escape Content = Yes (default) */2/* No extra code needed — OutSystems escapes automatically */3Customer.Name /* Will render as: John & Jane */45/* UNSAFE: Escape Content = No without manual encoding */6Customer.Name /* Renders raw HTML — XSS risk if name contains <script> */78/* SAFE: Escape Content = No with manual encoding when HTML is intentional */9/* Use only when you WANT to render some HTML but encode user content: */10"<b>" + EncodeHtml(Customer.Name) + "</b>: " + SanitizedHTMLContent1112/* Other encoding functions for specific contexts */13EncodeUrl(UserInput) /* For URL parameters */14EncodeJavaScript(UserInput) /* For values in JavaScript strings */Expected result: Every Expression widget in your application either uses Escape Content = Yes (default) or has a documented justification and manual EncodeHtml() calls for any untrusted content.
Review All SQL Elements for Expand Inline Usage
Review All SQL Elements for Expand Inline Usage
The most dangerous OutSystems security configuration is **Expand Inline = Yes** on SQL query parameters. It concatenates the value directly into the SQL string, bypassing parameterization and enabling SQL injection. To audit: In Service Studio, use **Ctrl+F** → search for `Expand Inline` with value `Yes` across your entire module. For each instance: 1. Double-click to navigate to the SQL element 2. Review why Expand Inline was used 3. If it's for a dynamic column name or table name — this is a legitimate use case but must use `EncodeSql()` 4. If it's for a value comparison — this is almost certainly wrong and should be parameterized instead TrueChange shows a yellow SQL Injection Warning for Expand Inline = Yes. These warnings MUST be reviewed; they are not safe to ignore.
1/* UNSAFE: String concatenation with Expand Inline = Yes */2/* SQL Parameter: SearchTerm, Expand Inline = Yes */3SELECT {Product}.[Name]4FROM {Product}5WHERE {Product}.[Name] LIKE '%@SearchTerm%'6/* If SearchTerm = "'; DROP TABLE Product;--" → SQL injection */789/* SAFE: Parameterized (Expand Inline = No, the default) */10/* SQL Parameter: SearchTerm, Expand Inline = No */11SELECT {Product}.[Name]12FROM {Product}13WHERE {Product}.[Name] LIKE '%' + @SearchTerm + '%'14/* Value is bound as a parameter — no injection possible */151617/* LEGITIMATE Expand Inline = Yes use case: dynamic IN clause */18/* SQL Parameter: IdList, Expand Inline = Yes */19/* Expression feeding IdList: BuildSafe_InClauseIntegerList(SelectedIds) */20SELECT {Product}.[Name]21FROM {Product}22WHERE {Product}.[Id] IN (@IdList)23/* BuildSafe_InClauseIntegerList() validates and formats: "1,2,3" — safe */242526/* LEGITIMATE use with EncodeSql for dynamic text */27/* Expression feeding DynamicValue: EncodeSql(UserInput) */28SELECT {Product}.[Name]29FROM {Product}30WHERE {Product}.[Name] = '@DynamicValue'31/* EncodeSql escapes single quotes: O'Brien → O''Brien */Expected result: Every Expand Inline = Yes parameter is either replaced with a parameterized query or uses EncodeSql()/BuildSafe_InClauseIntegerList() with justification. TrueChange SQL Injection Warnings are zero or explicitly reviewed.
Verify CSRF Protection Configuration
Verify CSRF Protection Configuration
**Traditional Web (O11):** CSRF protection is automatic via the hidden `__OSVSTATE` field embedded in every form. Ensure: - No forms use GET method for state-changing operations - The `__OSVSTATE` field is not stripped by custom HTML or JavaScript - JavaScript-driven AJAX calls include the viewstate token **Reactive Web (O11 and ODC):** CSRF protection works differently — Reactive apps use stateless HTTP calls. Protection is provided via: - **HTTPS**: All communication must be HTTPS (prevents token theft) - **SameSite cookie attribute**: OutSystems sets session cookies with SameSite=Lax by default - **Custom API endpoints**: For exposed REST APIs, implement token validation in the OnAuthentication callback Verify HTTPS is enforced: In Service Center → Factory → Modules → [your module] → check that there is no HTTP fallback. The server-level IIS should redirect HTTP → HTTPS.
1/* For exposed REST APIs — validate request origin in OnAuthentication */2/* Logic tab → Integrations → [Your REST API] → OnAuthentication */34Start5→ GetRequestHeader6 HeaderName = "X-API-Key"7→ GetAPIKeyRecord /* Aggregate: filter APIKey.Value = GetRequestHeader.Value */8→ If GetAPIKeyRecord.List.Empty9 [True]10 → Raise Exception11 Exception = User Exception12 Message = "Unauthorized: Invalid or missing API key"13→ End /* [False] — valid key, request proceeds */1415/* For JWT-protected endpoints */16/* Use JWT Forge component + validate in OnAuthentication */17/* ValidateJWT action from JWT component */18Start19→ GetRequestHeader (HeaderName = "Authorization")20→ If Index(GetRequestHeader.Value, "Bearer ", 0) <> 021 [True] → Raise Exception (Unauthorized)22→ ValidateJWT23 Token = Substr(GetRequestHeader.Value, 7, Length(GetRequestHeader.Value))24→ EndExpected result: All HTTPS redirects are in place. Exposed REST APIs require valid authentication tokens. Traditional Web forms include the OSVSTATE field and no client-side code strips it.
Implement Input Validation and Output Encoding
Implement Input Validation and Output Encoding
Beyond framework-level protections, implement defense-in-depth with explicit validation: **Server-side input validation** (in Server Actions): - Validate lengths: `Length(Trim(UserInput)) > 0 And Length(UserInput) <= 255` - Validate email format: use Email data type on the entity attribute (auto-validated) - Validate numeric ranges: `Amount >= 0 And Amount <= 1000000` - Reject control characters: use `Replace()` to strip `Chr(0)` through `Chr(31)` from free-text inputs before storing **File upload security** (if using Upload widget): - Validate file extension: `Index(ToLower(FileName), ".exe") = -1 And Index(ToLower(FileName), ".php") = -1` - Validate MIME type via the file header bytes, not just the extension - Store uploaded files as Binary Data in database — never write to the server filesystem - Enforce MaxFileSize on the Upload widget (in bytes: 10485760 = 10MB) **Output encoding**: - Use `EncodeUrl()` for all user-supplied values added to URLs - Use `EncodeJavaScript()` for values injected into JavaScript strings in JavaScript nodes
1/* Input validation expressions — use in If widgets or Server Actions */23/* Validate non-empty trimmed text with max length */4Length(Trim(UserInput)) > 0 And Length(UserInput) <= 50056/* Validate positive integer in range */7Quantity > 0 And Quantity <= 999989/* Validate file extension (allow only PDF and images) */10Index(ToLower(FileName), ".pdf") >= 011Or Index(ToLower(FileName), ".jpg") >= 012Or Index(ToLower(FileName), ".png") >= 01314/* Strip dangerous characters from free text */15Replace(Replace(UserInput, Chr(60), ""), Chr(62), "") /* Remove < > */1617/* URL-encode user values used in links */18"/products/search?q=" + EncodeUrl(SearchTerm)1920/* Safe JavaScript injection */21/* In JavaScript node: */22/* var userName = " + EncodeJavaScript(UserName) + "; */Expected result: Server Actions validate all user inputs before processing. File uploads are restricted to allowed types and stored as binary data. All user-supplied values in URLs and JavaScript contexts are encoded.
Run AI Mentor Studio Security Analysis
Run AI Mentor Studio Security Analysis
AI Mentor Studio provides automated taint analysis across your entire O11 or ODC codebase — it traces user-supplied data from input points to output/storage points and flags unprotected paths. **To run in O11:** Open your browser → navigate to AI Mentor Studio (your environment URL + `/AIAdvisor`). Click **Infrastructure** → select your module(s) → click **Analyze Now**. In the **Security** category, look for: - **SQL Injection** findings (Expand Inline with user input) - **XSS** findings (Escape Content = No with user input) - **Exposed Secrets** (API keys or passwords in Site Properties with default values visible) - **Insecure Direct Object References** (screens accessible without role checks) For each finding: click the item → Service Studio opens at the exact location. Fix the issue, republish, and re-run the analysis. **ODC:** AI Mentor Studio is integrated into ODC Portal → App Security. It also includes a **1-Click Patch** feature for certain vulnerability types.
Expected result: AI Mentor Studio shows zero Critical security findings. All High findings have been addressed or have documented risk acceptance. The Security score in the AI Mentor Studio dashboard shows green.
Apply HTTP Security Headers
Apply HTTP Security Headers
OutSystems does not configure all recommended security headers by default. Add these in your IIS or load balancer configuration (O11) or via OutSystems environment settings. **For O11 via Service Center:** Go to Service Center → Factory → Modules → [your module] → Properties. You cannot add headers here — headers must be added at the IIS level. For IIS-level headers (server team task), ensure these are configured: - `Strict-Transport-Security: max-age=31536000; includeSubDomains` (HSTS) - `X-Content-Type-Options: nosniff` - `X-Frame-Options: SAMEORIGIN` (prevents clickjacking) - `Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'` (review OutSystems' inline script requirements) - `Referrer-Policy: strict-origin-when-cross-origin` **Alternative for application-level headers:** Use an OnBeforeResponse callback on an exposed REST API, or use a JavaScript node to set `<meta>` equivalent headers for SPA contexts.
1/* HTTP Security Headers reference for IIS web.config */2/* Add to system.webServer → httpProtocol → customHeaders */34/*5<add name="Strict-Transport-Security"6 value="max-age=31536000; includeSubDomains" />7<add name="X-Content-Type-Options" value="nosniff" />8<add name="X-Frame-Options" value="SAMEORIGIN" />9<add name="Referrer-Policy" value="strict-origin-when-cross-origin" />10*/1112/* OutSystems already sets: */13/* X-XSS-Protection: 1; mode=block */14/* Content-Type with correct charset */1516/* CSP must be tested carefully — OutSystems uses */17/* inline scripts and eval() in generated code */18/* Start with report-only mode: */19/*20<add name="Content-Security-Policy-Report-Only"21 value="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; report-uri /csp-report" />22*/Expected result: Security header scanner (e.g., securityheaders.com) shows grade A or A+ for your OutSystems application URL. HSTS is active. Clickjacking protection is verified.
Complete working example
1/* ============================================================2 OutSystems Security — Quick Reference Expressions3 ============================================================ */456/* === XSS Prevention === */78/* Expression widget default — safe */9/* Escape Content = Yes (default) — no code needed */1011/* Manual encoding when Escape Content = No */12EncodeHtml(UserSuppliedText)13EncodeUrl(UserSuppliedUrlParam)14EncodeJavaScript(UserSuppliedJsValue)151617/* === SQL Injection Prevention === */1819/* Aggregates — always safe (parameterized by default) */20/* SQL element with Expand Inline = No — safe */21/* SQL element with Expand Inline = Yes — use these: */2223EncodeSql(UserText) /* Escapes single quotes */24BuildSafe_InClauseIntegerList(IntegerList) /* Safe IN clause: "1,2,3" */25BuildSafe_InClauseTextList(TextList) /* Safe IN clause: "'a','b'" */2627/* Example usage */28/* SQL text: WHERE {Product}.[Name] = '@SafeName' */29/* Parameter SafeName expression: EncodeSql(SearchInput) */303132/* === Role Checks === */3334/* Screen protection — Roles property (not code) */35/* Widget visibility */36CheckManagerRole()37CheckAdminRole() Or CheckManagerRole()3839/* Server Action guard */40If Not CheckAdminRole() Then41 Raise Exception(Security Exception, "Unauthorized")42End If434445/* === Input Validation === */4647/* Non-empty with max length */48Length(Trim(Input)) > 0 And Length(Input) <= 10004950/* Safe file extension check */51Index(ToLower(FileName), ".exe") = -152And Index(ToLower(FileName), ".js") = -153And Index(ToLower(FileName), ".php") = -15455/* Positive number range */56Amount > 0 And Amount <= 999999.99575859/* === CSRF (Exposed REST API) === */6061/* OnAuthentication callback — API key check */62GetRequestHeader(HeaderName: "X-API-Key").Value = ExpectedApiKey636465/* === Sensitive Data === */6667/* NEVER store in Client Variables (localStorage) */68/* NEVER log: passwords, tokens, PII */69/* Use Site Properties (O11) or Secret Settings (ODC) for API keys */70/* Secret Settings (ODC) — encrypted at rest, never visible in ODC Studio */Common mistakes
Why it's a problem: Setting Expand Inline = Yes on a SQL parameter to enable LIKE search, then feeding it directly from a user input field without EncodeSql().
How to avoid: Either use Expand Inline = No with '%' + @SearchTerm + '%' (parameterized LIKE), or if Expand Inline is needed use: EncodeSql(SearchInput) as the parameter expression and keep the Expand Inline = Yes setting.
Why it's a problem: Setting Escape Content = No on an Expression widget to render bold text, but displaying a user's profile name in the same expression without encoding.
How to avoid: If you need to mix HTML formatting with user content, use Escape Content = Yes and apply inline CSS via Style Classes instead of HTML tags — or explicitly encode the user portion: "<b>" + EncodeHtml(UserName) + "</b>".
Why it's a problem: Storing third-party API keys as Site Property default values, making them visible to any developer with Service Studio access to the module.
How to avoid: In O11, set the Default Value to empty and configure the actual key per-environment in Service Center. In ODC, use Secret Settings (encrypted) — the value is never visible after being set.
Why it's a problem: Relying solely on screen-level Roles property for security without adding Check<Role>Role() guards in Server Actions, leaving API endpoints unprotected.
How to avoid: Apply defense in depth: protect both the screen (Roles property) AND the Server Action (CheckManagerRole() If guard) — screens are navigation protection, Server Actions are the actual data protection layer.
Best practices
- Treat Expand Inline = Yes as a code smell that requires a security review — run a monthly search for it and document every legitimate use case.
- Escape Content = No on Expression widgets should trigger a peer code review; add it to your team's pull request checklist.
- Store API keys, connection strings, and secrets in Site Properties (O11) or Secret Settings (ODC) — never hardcode them in action logic or entity default values.
- Use AI Mentor Studio's Exposed Secrets detection to find API keys accidentally stored in visible Site Properties or in logs.
- Never log sensitive data (passwords, tokens, credit card numbers, PII) in Service Center error logs — use masked placeholders in log messages.
- Implement server-side pagination with Max Records on all Aggregates exposed to user-controlled search inputs — unlimited result sets can be abused for data harvesting.
- In ODC, leverage the App Security dashboard in ODC Portal for continuous vulnerability monitoring — it includes 1-Click patching for common issues.
- Review and restrict the Roles property on every screen annually — screens accidentally left as Anonymous after development are a common exposure.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm auditing the security of an OutSystems 11 application. Help me create a security checklist covering: (1) finding and fixing Expand Inline = Yes in SQL elements using EncodeSql() and BuildSafe_InClauseIntegerList(), (2) auditing Expression widgets for Escape Content = No with unsafe user content, (3) securing exposed REST APIs with OnAuthentication token validation, (4) using CheckRole() guards in Server Actions beyond just screen-level Roles property, and (5) running AI Mentor Studio taint analysis. Use exact Service Studio UI paths and OutSystems expression syntax.
In my OutSystems Service Studio module, I need to fix a SQL injection vulnerability. I have a SQL element with an Expand Inline = Yes parameter being fed directly from a search input. Show me: (1) how to convert it to a parameterized query with Expand Inline = No for the LIKE case, (2) when I legitimately need Expand Inline = Yes for an IN clause, how to use BuildSafe_InClauseIntegerList(), and (3) where to find the TrueChange SQL Injection Warning in the TrueChange tab and what it means.
Frequently asked questions
Does OutSystems automatically protect against SQL injection in Aggregates?
Yes. Aggregates always use parameterized queries — user-supplied filter values are bound as parameters and never concatenated into SQL. The SQL injection risk only exists in the SQL query element when Expand Inline = Yes is set on a parameter. Aggregates have no equivalent escape hatch and are always safe.
What does EncodeSql() actually do?
EncodeSql() escapes single quotes by doubling them (e.g., O'Brien becomes O''Brien) so they cannot break out of a SQL string literal. It ONLY works for string literals inside single quotes — it does not protect against injection in other SQL contexts like column names or table names.
Is AI Mentor Studio free to use?
AI Mentor Studio is included with all OutSystems 11 environments at no extra cost — access it at your environment URL + /AIAdvisor. In ODC, the App Security dashboard is included in the platform. The Agent Workbench security guardrails (profanity filtering, PII detection, prompt injection prevention) are part of the Agent Workbench add-on priced at €20,000/year.
How does OutSystems handle Content Security Policy (CSP) headers?
OutSystems does not configure CSP headers by default because the platform's generated code uses inline JavaScript and eval(), which would require 'unsafe-inline' and 'unsafe-eval' in the policy. Implement CSP in report-only mode first to collect violations, then work with your server team to configure IIS headers accordingly. The generated code patterns vary by OutSystems version — test CSP changes in a non-production environment.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation