From e6987e084cf63666d2228d128212e747738cd060 Mon Sep 17 00:00:00 2001 From: "Rondineli G. Saad" Date: Tue, 22 Sep 2026 19:24:00 -0300 Subject: [PATCH] fix(security): mitiga vulnerabilidades prioritarias --- .../vulnerability-assessment-2026-09.md | 199 ++++++++++++++++++ .../services/articleRequestGraphicPage.php | 17 +- .../scielo-org/pages/services/sendMail.php | 15 +- htdocs/classRequestVars.php | 13 +- htdocs/classScieloBase.php | 72 ++++--- htdocs/export.php | 71 +++++-- htdocs/maintain.php | 8 +- htdocs/old2new.inc | 9 +- htdocs/rss.php | 75 +++++-- htdocs/scielo.php | 16 +- htdocs/scieloOrg/php/clinicaltrials.php | 12 +- htdocs/scieloOrg/php/projfapesp.php | 12 +- htdocs/scielolog.php | 2 +- htdocs/scielologofi.php | 2 +- htdocs/security.php | 133 ++++++++++++ tests/security_regression.php | 65 ++++++ 16 files changed, 631 insertions(+), 90 deletions(-) create mode 100644 docs/security/vulnerability-assessment-2026-09.md create mode 100644 htdocs/security.php create mode 100644 tests/security_regression.php diff --git a/docs/security/vulnerability-assessment-2026-09.md b/docs/security/vulnerability-assessment-2026-09.md new file mode 100644 index 000000000..e98dfc909 --- /dev/null +++ b/docs/security/vulnerability-assessment-2026-09.md @@ -0,0 +1,199 @@ +# Avaliacao tecnica do relatorio de vulnerabilidades - setembro de 2026 + +## Objetivo e limites + +Este documento confronta os 11 achados do relatorio `Res_Seg_Scielo_09_2026.pdf` +com o codigo presente na branch `codex/security-remediation-sep-2026`. + +A avaliacao e estatica e nao destrutiva. Nenhuma prova de conceito foi executada +contra ambientes externos. As evidencias de execucao apresentadas no relatorio +foram consideradas evidencias do auditor, e nao foram repetidas. + +Foram revisados 17 arquivos de implementacao, configuracao e artefatos diretamente +relacionados aos achados. Tambem foi feita uma varredura de padroes de segredo nos +arquivos rastreados e nos artefatos locais de texto. + +Os valores CVSS sao preliminares e usam apenas metricas Base do CVSS 4.0. Devem ser +recalculados com as metricas ambientais depois de confirmar arquitetura, dados +alcancaveis, controles de borda e importancia dos ativos. Referencia: +https://www.first.org/cvss/v4.0/specification-document + +## Sumario executivo + +- 9 achados foram confirmados diretamente no codigo ou na configuracao de publicacao. +- 2 achados foram confirmados apenas de forma condicional: 008 e 011. +- 1 caminho adicional de SSRF foi identificado nos endpoints de servicos por meio do + parametro `caller`. +- Nao foram encontrados segredos reais hardcoded pelos padroes de alta confianca. + O template de SMTP possui campos de usuario e senha vazios. +- Veredicto: **BLOQUEADO para publicacao sem correcoes**. + +As correcoes obrigatorias antes de uma nova publicacao sao: remover os modos de +diagnostico publicos, eliminar destinos HTTP controlados pelo cliente, parar de +exportar dados de request para `$GLOBALS`, corrigir as reflexoes HTML e restringir +arquivos e executaveis legados no Apache. + +## Estado da remediacao na branch + +Em 21 de setembro de 2026, os itens P0 abaixo foram implementados e validados +localmente. Eles ainda dependem de reteste no ambiente de homologacao antes de serem +considerados encerrados: + +- 001 e 005: `debug` e `diag` foram bloqueados para requisicoes externas. O modo + `debug=xml`, necessario ao fluxo legado de estatisticas, permanece disponivel apenas + em loopback e responde como `text/plain`. Os demais modos exigem habilitacao + explicita e loopback. +- 002: `old2new.inc` deixou de copiar chaves de GET, POST, COOKIE, SERVER, ENV e + SESSION para `$GLOBALS`. Permaneceram somente aliases conhecidos dos arrays legados. +- 003: `export.php` passou a usar `SERVER_SCIELO` validado, formatos e PID em allowlist, + query codificada, timeout e bloqueio de redirecionamento. +- A-01: `articleRequestGraphicPage.php` e `sendMail.php` ignoram o host recebido em + `caller` e usam somente `SERVER_SCIELO` validado. +- Variantes: `rss.php`, `clinicaltrials.php` e `projfapesp.php` tambem deixaram de usar + `HTTP_HOST` como destino de requisicoes do servidor. +- Eventos negados e falhas de backend sao registrados em JSON sem payloads, e-mails, + cookies, tokens ou valores de configuracao. + +Validacoes executadas: + +- lint PHP 8.5 em 15 arquivos: aprovado; +- `tests/security_regression.php`: aprovado; +- smoke HTTP em container local: home, exportacao, RSS, ensaios clinicos e projetos + FAPESP retornaram HTTP 200 mesmo com `Host` adulterado; +- `debug` e `diag` externos nao exibiram marcadores de diagnostico; +- `debug=xml` via loopback retornou HTTP 200 e `text/plain`; +- `git diff --check`: aprovado. + +Os endpoints `articleRequestGraphicPage.php` e `sendMail.php` ainda retornam HTTP 500 +em PHP 8.5 tanto no caso de controle quanto no caso com `caller` adulterado. O log +atribui a falha preexistente a uma chamada estatica de metodo nao estatico na biblioteca +DOMIT. A resposta confirma que o backend usado foi o host configurado, mas a correcao +de compatibilidade DOMIT deve preceder o reteste funcional completo desses dois fluxos. + +## Matriz de validacao e risco + +| ID | Status no codigo | Evidencia principal | CWE | CVSS 4.0 Base preliminar | Prioridade interna | +| --- | --- | --- | --- | --- | --- | +| 001 | Mitigado na branch; aguarda homologacao | O modo debug publico foi bloqueado; XML de compatibilidade ficou restrito a loopback e todos os diagnosticos usam `text/plain` | CWE-79, CWE-489 | 6.3 Media - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N` | P0 | +| 002 | Mitigado na branch; aguarda homologacao | `old2new.inc` nao exporta mais chaves arbitrarias para `$GLOBALS`; `classRequestVars.php` usa superglobais explicitas | CWE-621, CWE-915 | 8.8 Alta - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:H/VA:L/SC:N/SI:N/SA:N` | P0 | +| 003 | Mitigado na branch; aguarda homologacao | `export.php` usa exclusivamente `SERVER_SCIELO` validado; variantes em RSS e paginas SciELO Org receberam a mesma protecao | CWE-918 | 8.8 Alta - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:L/VA:L/SC:N/SI:N/SA:N` | P0 | +| 004 | Confirmado | `statjournal.php:33-39` nao altera `$_GET`; `statjournal.php:186-220` insere `collection` em atributos `href` sem escape | CWE-79 | 5.1 Media - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N` | P1 | +| 005 | Mitigado na branch; aguarda homologacao | `diag` exige habilitacao explicita e loopback; tentativas externas seguem o fluxo normal e geram auditoria sem dados sensiveis | CWE-200, CWE-489 | 6.9 Media - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N` | P0 | +| 006 | Confirmado | `httpd-scielo.conf:27-30` concede acesso publico e execucao a todo o diretorio `cgi-bin`; nao existe regra especifica para WXIS | CWE-200, CWE-284 | 6.9 Media - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N` | P1 | +| 007 | Confirmado | `httpd-scielo.conf:11-19` publica todo o DocumentRoot e bloqueia somente `phpinfo.php`/`info.php`; os arquivos citados ficam dentro de `htdocs` | CWE-552 | 6.3 Media - `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N` | P1 | +| 008 | Condicional | `mimetex.cgi` existe e `httpd-scielo.conf:27-30` permite CGI; o arquivo e um executavel PE32 para Windows e tende a falhar no Rocky Linux | CWE-1104 | N/A ate identificar versao/CVE e ambiente em que o binario pode executar | P1 - remover | +| 009 | Parcialmente confirmado | `scielolog.php`, `scielologofi.php` e `sci_statart.php` existem e dependem de globais legadas; o HTTP 500 nao foi reproduzido localmente | CWE-1050 | N/A - superficie legada sem impacto exploravel demonstrado | P2 | +| 010 | Confirmado | `articleRequestGraphicPage.php:17-33,44,109-123` reflete `pid`, `caller`, `lang` e anos em HTML/JavaScript sem codificacao contextual | CWE-79 | 5.1 Media - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N` | P1 | +| 011 | Condicional | `sendMail.php:24-36,41-47,80-104` aceita destinatario, Reply-To, nomes e comentario sem autenticacao, rate limit ou validacao no servidor | CWE-400, CWE-799, CWE-20 | 6.3 Media - `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:L/SC:L/SI:L/SA:L` | P1 se SMTP ativo; P2 se desativado | +| A-01 | Mitigado na branch; reteste funcional bloqueado por DOMIT | Os endpoints ignoram `caller` como host e usam `SERVER_SCIELO`; testes confirmaram o destino configurado antes da falha preexistente da biblioteca DOMIT | CWE-918 | 8.8 Alta - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:L/VA:L/SC:N/SI:N/SA:N` | P0 | + +### Observacoes sobre as severidades + +O XSS 001 foi classificado como alto no relatorio. No CVSS Base 4.0 preliminar ele +resulta em 6.3 porque exige interacao ativa da vitima e o impacto ocorre +principalmente no navegador. A prioridade interna permanece P0 porque o endpoint e +central, usa o dominio institucional e a funcionalidade de debug tambem divulga +informacoes internas. + +O achado 002 e uma fraqueza sistemica confirmada. O impacto exato depende dos sinks +alcancados por cada global sobrescrita, mas a possibilidade de alterar variaveis de +controle sem autenticacao justifica a prioridade P0. + +O SSRF 003 tem evidencia dinamica no relatorio e correspondencia direta no codigo. O +impacto deve ser refinado testando somente em homologacao se o processo alcanca rede +interna, metadata de nuvem ou portas administrativas. + +## Verificacao dos oito dominios NSI.04 + +| Dominio | Resultado | +| --- | --- | +| Injecao | SSRF confirmado em `export.php` e no fluxo `caller -> ArticleService`; concatenacao manual de parametros tambem aparece em varios servicos legados | +| Autenticacao e sessao | Os endpoints avaliados sao publicos; `sendMail.php` nao possui controle antiabuso no servidor | +| XSS | Confirmado nos IDs 001, 004 e 010; a CSP atual permite `unsafe-inline` e `unsafe-eval`, reduzindo sua utilidade como defesa adicional | +| Controle de acesso | Debug, diagnostico, WXIS, CGI e arquivos auxiliares estao acessiveis sem restricao especifica | +| Validacao de entrada | Predomina uso direto de `$_REQUEST`; validacao client-side em `sendMail.php` nao protege o servidor | +| Criptografia e dados | Nenhum problema criptografico novo foi necessario para confirmar os 11 achados; SMTP usa configuracao externa e o template nao contem senha | +| Configuracao de seguranca | `Require all granted` abrange `htdocs` e `cgi-bin`; a CSP permite origens HTTP/HTTPS e execucao inline/eval | +| Logging | Nao ha trilha de auditoria ou telemetria antiabuso suficiente nos fluxos avaliados; adicionar logs sem registrar payloads, e-mails ou tokens | + +## Plano de tratamento + +### P0 - antes da proxima publicacao + +1. Remover o processamento publico de `debug` e `diag`. Se diagnostico operacional + continuar necessario, usar feature flag desativada por padrao, autenticacao forte, + allowlist de rede e resposta sem paths, XML ou URLs internas. +2. Em `export.php`, usar host interno vindo de configuracao confiavel. Validar o PID e + o formato por allowlist, montar query com `http_build_query` e aplicar timeout. +3. Remover a copia para `$GLOBALS` em `old2new.inc`. Manter temporariamente apenas os + aliases de arrays legados e migrar consumidores para superglobais explicitas. +4. Remover o parametro `caller` como origem do host. Resolver o dominio por + configuracao/tenant conhecido e usar allowlist exata, com bloqueio de IPs privados, + loopback, link-local e redirecionamentos. + +### P1 - mesma janela de remediacao + +1. Aplicar `htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')` em + contextos HTML e `json_encode` para valores inseridos em JavaScript. +2. Restringir `cgi-bin` no Apache aos executaveis realmente necessarios e impedir + acesso externo direto ao WXIS quando a aplicacao puder chama-lo localmente. +3. Bloquear no servidor web extensoes e nomes auxiliares: `.inc`, `.def`, `.template`, + arquivos de versao e documentacao operacional. +4. Remover `mimetex.cgi` do artefato Linux se nao houver dependencia funcional. +5. Em `sendMail.php`, validar todos os campos no servidor, exigir POST e protecao CSRF, + fixar o dominio do artigo, limitar tamanho, aplicar rate limit e CAPTCHA quando + apropriado. Nao retornar detalhes internos do PHPMailer. + +### P2 - reducao de superficie e manutencao + +1. Remover ou reimplementar `scielolog.php`, `scielologofi.php` e `sci_statart.php`. +2. Inventariar bibliotecas vendorizadas antigas e substituir componentes sem suporte. +3. Tornar a CSP restritiva, removendo `unsafe-eval`, `unsafe-inline`, `http:` e o + curinga implicito de hosts HTTPS apos inventariar dependencias legitimas. + +## Plano seguro de reteste em homologacao + +### Regras gerais + +- Executar somente em ambiente autorizado, com dados sinteticos e destino de rede + controlado pela equipe. +- Registrar ID do caso, commit, data, requisicao, codigo HTTP, trecho minimo da + resposta e logs correlacionados. +- Nao usar cookies reais, e-mails de terceiros, metadata cloud real nem sistemas + internos de producao. +- Cada teste deve ter um caso positivo funcional e um caso negativo de seguranca. + +### Casos por achado + +| ID | Procedimento de reteste | Criterio de aprovacao | +| --- | --- | --- | +| 001 | Solicitar a pagina com `debug` e um marcador HTML inerte em parametro comum | `debug` e ignorado ou retorna 403/404; marcador nunca e interpretado; nenhuma informacao interna aparece | +| 002 | Enviar chaves com nomes de variaveis internas e executar testes unitarios do adaptador de request | Nenhuma chave externa e criada em `$GLOBALS`; `$_SERVER` e variaveis internas permanecem inalteradas | +| 003 | Enviar `Host` apontando para um mock HTTP controlado, com e sem porta alternativa | Nenhuma conexao chega ao mock; o backend usa somente o destino configurado; Host invalido retorna 400 | +| 004 | Inserir marcador contendo aspas e sinais `<`/`>` em `collection` | O HTML possui entidades, o DOM nao cria novos atributos/elementos e os links continuam validos | +| 005 | Solicitar `diag=1` e variantes de caixa/metodo | Resposta 403/404 ou fluxo normal, sem path, XML, URI interna ou URL WXIS | +| 006 | Acessar WXIS externamente e executar o fluxo normal da aplicacao | Acesso direto externo e negado; chamada interna autorizada continua funcional | +| 007 | Solicitar cada arquivo citado e extensoes equivalentes | Todos retornam 403/404; CSS, imagens e demais estaticos permitidos continuam acessiveis | +| 008 | Solicitar `mimetex.cgi` e verificar a imagem final/container | Endpoint retorna 404 e o arquivo nao existe no artefato implantado | +| 009 | Solicitar os tres endpoints legados | Endpoints removidos retornam 404; se mantidos, retornam resposta controlada sem stack trace e possuem testes proprios | +| 010 | Inserir marcador inerte em `pid`, `caller`, `lang`, `startYear` e `lastYear` | Valores sao validados e codificados conforme HTML/JS; nenhum novo atributo ou elemento e criado | +| 011 | Usar caixa postal e SMTP de teste, exceder o limite configurado e enviar campos invalidos | Destinos invalidos sao rejeitados; limite produz 429; somente mensagens autorizadas chegam ao sink de teste; nenhuma quebra de cabecalho/HTML ocorre | +| A-01 | Apontar `caller` para mock HTTP, loopback e IP privado sintetico | Nenhuma conexao externa ocorre; somente dominios mapeados na configuracao sao aceitos e redirecionamentos sao revalidados | + +## Evidencias requeridas para encerramento + +Um achado somente deve ser marcado como corrigido quando houver: + +1. commit ou pull request identificando a alteracao; +2. teste automatizado que falhe no codigo vulneravel e passe no codigo corrigido; +3. evidencia de reteste em homologacao; +4. verificacao de regressao do fluxo funcional; +5. aprovacao do responsavel tecnico e do responsavel de seguranca. + +## Resultado da deteccao de segredos + +**APROVADO para esta etapa de analise**, com o seguinte limite: a varredura procurou +chaves privadas, tokens AWS/GitHub/Slack/Google, JWTs e URLs de conexao com senha. +Nenhum valor real de alta confianca foi encontrado. Campos de senha vazios, argumentos +de bibliotecas vendorizadas e exemplos sem credencial real foram tratados como falsos +positivos. Uma nova varredura deve ser executada antes de cada commit e pull request. diff --git a/htdocs/applications/scielo-org/pages/services/articleRequestGraphicPage.php b/htdocs/applications/scielo-org/pages/services/articleRequestGraphicPage.php index 2d5fd43c2..a6cbdc6cc 100644 --- a/htdocs/applications/scielo-org/pages/services/articleRequestGraphicPage.php +++ b/htdocs/applications/scielo-org/pages/services/articleRequestGraphicPage.php @@ -4,7 +4,8 @@ require_once($DirNameLocalGraphPage."../../users/functions.php"); require_once($DirNameLocalGraphPage."../../users/langs.php"); - require_once($DirNameLocalGraphPage."../../../../php/include.php"); + require_once($DirNameLocalGraphPage."../../../../php/include.php"); + require_once($DirNameLocalGraphPage."../../../../security.php"); require_once($DirNameLocalGraphPage."../../classes/services/ArticleServices.php"); require_once(dirname(__FILE__)."/../../classes/services/AccessServiceBar.php"); require_once(dirname(__FILE__)."/../../classes/Open_Flash_Chart/ofc-library/open_flash_chart_object.php"); @@ -14,8 +15,18 @@ $scielodef = parse_ini_file($DirNameLocalGraphPage."/../../scielo.def.php", true); $scielomaindef = parse_ini_file($DirNameLocalGraphPage."/../../../../scielo.def.php", true); - $pid = $_REQUEST['pid']; - $caller = $_REQUEST["caller"]; + $pid = isset($_REQUEST['pid']) ? $_REQUEST['pid'] : ''; + $requestedCaller = isset($_REQUEST['caller']) ? $_REQUEST['caller'] : ''; + $caller = scielo_internal_host_from_config($scielomaindef); + if ($requestedCaller !== '' && $requestedCaller !== $caller) { + scielo_audit_event( + 'access.denied', + 'unauthorized', + 'service.article_metadata', + null, + array('reason' => 'caller_override_ignored') + ); + } $articleService = new ArticleService($caller); $articleService->setParams($pid); diff --git a/htdocs/applications/scielo-org/pages/services/sendMail.php b/htdocs/applications/scielo-org/pages/services/sendMail.php index e6f3874f7..e94bf51f4 100644 --- a/htdocs/applications/scielo-org/pages/services/sendMail.php +++ b/htdocs/applications/scielo-org/pages/services/sendMail.php @@ -4,7 +4,8 @@ require_once(dirname(__FILE__)."/../../users/langs.php"); require_once(dirname(__FILE__)."/../../includes/phpmailer/class.phpmailer.php"); require_once(dirname(__FILE__)."/../../classes/services/ArticleServices.php"); -require_once(dirname(__FILE__)."/../../../../php/include.php"); +require_once(dirname(__FILE__)."/../../../../php/include.php"); +require_once(dirname(__FILE__)."/../../../../security.php"); $bvsSiteIni = parse_ini_file(dirname(__FILE__)."/../../../../bvs-site-conf.php",true); @@ -25,7 +26,17 @@ $acao = $cgi["acao"]; $pid = $cgi["pid"]; -$caller = $cgi["caller"]; +$requestedCaller = isset($cgi['caller']) ? $cgi['caller'] : ''; +$caller = scielo_internal_host_from_config($mainscielodef); +if ($requestedCaller !== '' && $requestedCaller !== $caller) { + scielo_audit_event( + 'access.denied', + 'unauthorized', + 'service.article_metadata', + null, + array('reason' => 'caller_override_ignored') + ); +} //geting metadatas from PID $articleService = new ArticleService($caller); diff --git a/htdocs/classRequestVars.php b/htdocs/classRequestVars.php index daa3977e5..827fa78c7 100644 --- a/htdocs/classRequestVars.php +++ b/htdocs/classRequestVars.php @@ -13,12 +13,13 @@ function __construct() function RequestVars () { - global $HTTP_GET_VARS, $HTTP_POST_VARS, $REQUEST_URI, $SCRIPT_NAME; + $requestUri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; + $scriptName = isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : ''; - if (strpos($REQUEST_URI, "?") === false) + if (strpos($requestUri, "?") === false) { - $QSCnav = $REQUEST_URI; - $QSCscript = $SCRIPT_NAME; + $QSCnav = $requestUri; + $QSCscript = $scriptName; $QSCnav = preg_replace('/^' . preg_quote($QSCscript, '/') . '/', '', $QSCnav); $QSCvars = explode("/", $QSCnav); $QSCArray = array(); @@ -38,11 +39,11 @@ function RequestVars () } } - $this->_request = array_merge($HTTP_GET_VARS, $HTTP_POST_VARS, $QSCArray); + $this->_request = array_merge($_GET, $_POST, $QSCArray); } else { - $this->_request = array_merge($HTTP_GET_VARS, $HTTP_POST_VARS); + $this->_request = array_merge($_GET, $_POST); } if (!isset($this->_request['lng']) || strpos("|en|pt|es|", $this->_request['lng']) == 0) { diff --git a/htdocs/classScieloBase.php b/htdocs/classScieloBase.php index a628da757..fc7b18714 100644 --- a/htdocs/classScieloBase.php +++ b/htdocs/classScieloBase.php @@ -1,7 +1,8 @@ _GetInfoByHost(); if ( !$this->_request->getRequestValue ("script", $this->_script) ) $this->_script = $this->_homepg; - $this->_request->getRequestValue ("debug", $this->_debug); - $this->_debug = strtoupper ($this->_debug); + $requestedDebug = ''; + if ($this->_request->getRequestValue("debug", $requestedDebug)) { + if (scielo_diagnostic_mode_allowed($requestedDebug)) { + $this->_debug = strtoupper($requestedDebug); + } else { + scielo_audit_event( + 'access.denied', + 'unauthorized', + 'diagnostics', + null, + array('reason' => 'remote_debug_disabled') + ); + } + } // Create a DefFile object to gather information from def file $this->_SetDefFileObject(); @@ -198,8 +211,12 @@ function _SetDefFileObject() Last Change: 19/06/2001 (Roberto) *************************************************************************/ - function _CheckBypassTransformer() - { + function _CheckBypassTransformer() + { + if (!scielo_diagnostic_mode_allowed($this->_debug)) { + return false; + } + if ( (!$this->_debug || $this->_debug == 'VERIFICA') && ($this->_script != 'sci_verify') ) { return false; @@ -227,19 +244,19 @@ function _CheckBypassTransformer() 05/07/2007 (Takenaka) 19/06/2001 (Roberto) *************************************************************************/ - function _BypassTransformer() - { - switch ($this->_debug){ - case "XML": - header("Content-type:text/xml; charset=utf-8\n"); - echo $this->_xml; + function _BypassTransformer() + { + header("Content-Type: text/plain; charset=UTF-8"); + switch ($this->_debug){ + case "XML": + echo $this->_xml; break; case "ON": $fd = fopen ($this->_IsisScriptUrl,"r"); if (!$fd) { - echo "
Could not open url: [".$this->_IsisScriptUrl."]\n
"; - return; + echo "Could not open diagnostic URL.\n"; + return; } $buf = ''; @@ -249,19 +266,14 @@ function _BypassTransformer() fclose($fd); echo $buf; break; - case "XSL": - echo $this->_xsl; - break; - default: - echo "
\n"; - echo "Generated XML
\n"; - echo '\n
"; - - echo "url of IsisScript=$this->_IsisScriptUrl
\n"; - echo "\$xsl=$this->_xsl
\n"; - break; + case "XSL": + echo $this->_xsl; + break; + default: + echo "Generated XML\n"; + echo $this->_xml . "\n"; + echo "XSL=" . $this->_xsl . "\n"; + break; } } @@ -409,7 +421,9 @@ function _Transform() function _TransformXML() { $result = ""; - $diag = isset($_REQUEST["diag"]) && $_REQUEST["diag"] == "1"; + $diag = isset($_REQUEST["diag"]) + && $_REQUEST["diag"] == "1" + && scielo_diagnostics_allowed(); // Apply transformer in xml $transform = new ScieloXMLTransformer(); diff --git a/htdocs/export.php b/htdocs/export.php index ca3bbd88d..5a41da903 100644 --- a/htdocs/export.php +++ b/htdocs/export.php @@ -1,21 +1,70 @@ - 'invalid_export_parameters') + ); print('Error: Exportation tool.
'); exit; } -$url = "http://".$_SERVER['HTTP_HOST']."/cgi-bin/wxis.exe/?IsisScript=ScieloXML/sci_artmetadata.xis&def=scielo.def.php&pid=".$pid."&"; - -$handle = fopen($url, "rb"); +$config = parse_ini_file(__DIR__ . '/scielo.def.php', true); +try { + $internalHost = scielo_internal_host_from_config($config); +} catch (RuntimeException $exception) { + http_response_code(500); + scielo_audit_event( + 'system.error', + 'failure', + 'article.export', + $pid, + array('reason' => 'invalid_internal_host_configuration') + ); + print('Error: Exportation service unavailable.
'); + exit; +} + +$query = http_build_query(array( + 'IsisScript' => 'ScieloXML/sci_artmetadata.xis', + 'def' => 'scielo.def.php', + 'pid' => $pid, +)); +$url = 'http://' . $internalHost . '/cgi-bin/wxis.exe/?' . $query; +$context = stream_context_create(array( + 'http' => array( + 'timeout' => 5, + 'follow_location' => 0, + ), +)); +$handle = @fopen($url, "rb", false, $context); +if ($handle === false) { + http_response_code(502); + scielo_audit_event( + 'system.error', + 'failure', + 'article.export', + $pid, + array('reason' => 'metadata_backend_unavailable') + ); + print('Error: Exportation service unavailable.
'); + exit; +} $xml = ""; do { @@ -36,10 +85,6 @@ $xsl .= "createBibTexReference.xsl"; break; - case "RefMan": - $xsl .= "createRefManReference.xsl"; - break; - case "EndNote": $xsl .= "createEndNoteReference.xsl"; break; diff --git a/htdocs/maintain.php b/htdocs/maintain.php index b3597dc34..27be7fb47 100644 --- a/htdocs/maintain.php +++ b/htdocs/maintain.php @@ -1,4 +1,10 @@ - + diff --git a/htdocs/old2new.inc b/htdocs/old2new.inc index 91265f3d3..7ab034732 100644 --- a/htdocs/old2new.inc +++ b/htdocs/old2new.inc @@ -50,13 +50,8 @@ $old2newArray = array( ); foreach ($old2newArray as $old => $new) { - if (!isset($$old) && isset($$new)) { - $$old = $$new; - } - if (isset($$old) && is_array($$old)) { - foreach ($$old as $key => $value) { - $GLOBALS[$key] = $value; - } + if (!isset($GLOBALS[$old]) && isset($GLOBALS[$new])) { + $GLOBALS[$old] = $GLOBALS[$new]; } } diff --git a/htdocs/rss.php b/htdocs/rss.php index bbd56e847..900d9b7ab 100644 --- a/htdocs/rss.php +++ b/htdocs/rss.php @@ -3,11 +3,15 @@ ob_start(); -require_once(dirname(__FILE__)."/class.XSLTransformer.php"); +require_once(dirname(__FILE__)."/class.XSLTransformer.php"); +require_once(dirname(__FILE__)."/security.php"); + +$config = parse_ini_file(__DIR__ . '/scielo.def.php', true); +$internalHost = scielo_internal_host_from_config($config); $pid = $_REQUEST['pid']; $lang = $_REQUEST['lang']; -$debug = $_REQUEST['debug']; +$debug = isset($_REQUEST['debug']) ? $_REQUEST['debug'] : ''; $pRelease = $_REQUEST['prelease']; $dateStart = $_REQUEST['dateStart']; $dateStop = $_REQUEST['dateStop']; @@ -21,7 +25,20 @@ */ if($pRelease){ - $url = "http://".$_SERVER['HTTP_HOST']."/cgi-bin/wxis.exe/?IsisScript=ScieloXML/pressreleaserss.xis&def=scielo.def.php&sln=$lang&script=sci_serial&pid=$pid&lng=$lang&nrm=iso&dateStart=$dateStart&dateStop=$dateStop&prelease=$pRelease&count=$count&date=$date"; + $url = 'http://' . $internalHost . '/cgi-bin/wxis.exe/?' . http_build_query(array( + 'IsisScript' => 'ScieloXML/pressreleaserss.xis', + 'def' => 'scielo.def.php', + 'sln' => $lang, + 'script' => 'sci_serial', + 'pid' => $pid, + 'lng' => $lang, + 'nrm' => 'iso', + 'dateStart' => $dateStart, + 'dateStop' => $dateStop, + 'prelease' => $pRelease, + 'count' => $count, + 'date' => $date, + )); $rss = file_get_contents($url); echo $rss; die(); @@ -35,7 +52,15 @@ $pid = substr($pid,0,9); - $url = "http://".$_SERVER['HTTP_HOST']."/cgi-bin/wxis.exe/?IsisScript=ScieloXML/sci_issues.xis&def=scielo.def.php&sln=$lang&script=sci_issues&pid=$pid&lng=$lang&nrm=iso"; + $url = 'http://' . $internalHost . '/cgi-bin/wxis.exe/?' . http_build_query(array( + 'IsisScript' => 'ScieloXML/sci_issues.xis', + 'def' => 'scielo.def.php', + 'sln' => $lang, + 'script' => 'sci_issues', + 'pid' => $pid, + 'lng' => $lang, + 'nrm' => 'iso', + )); $xml = file_get_contents($url); $cortado = strstr($xml, ' 'ScieloXML/sci_issuerss.xis', + 'def' => 'scielo.def.php', + 'sln' => 'en', + 'script' => 'sci_issuetoc', + 'pid' => $pid, + 'lng' => $lang, + 'nrm' => 'iso', +)); $xml = file_get_contents($url); $xsl = dirname(__FILE__)."/xsl/createRSS.xsl"; -if(isset($debug)) { - - echo '

XML

'; - echo ''; - - echo '

XSL

'; - echo ''; - die(); -} +if ($debug !== '' && scielo_diagnostic_mode_allowed($debug)) { + + header('Content-Type: text/plain; charset=UTF-8'); + echo "XML\n"; + echo $xml; + echo "\nXSL\n"; + echo $xsl; + die(); +} + +if ($debug !== '') { + scielo_audit_event( + 'access.denied', + 'unauthorized', + 'diagnostics', + null, + array('reason' => 'remote_rss_debug_disabled') + ); +} $t = new XSLTransformer(); $t->setXml($xml); diff --git a/htdocs/scielo.php b/htdocs/scielo.php index fd5a62e03..fcb1c93cd 100644 --- a/htdocs/scielo.php +++ b/htdocs/scielo.php @@ -118,7 +118,8 @@ function is_requested_language_available($sxml) { $xsl = $scielo->GenerateXslUrl(); $scielo->SetXSLUrl ($xsl); - if ((isset($_REQUEST['diag']) ? $_REQUEST['diag'] : '') == '1') { + if ((isset($_REQUEST['diag']) ? $_REQUEST['diag'] : '') == '1' + && scielo_diagnostics_allowed()) { $xslExists = file_exists($xsl) ? 'yes' : 'no'; $xmlPreview = htmlspecialchars(substr($xml, 0, 12000)); $errorTagCount = preg_match_all('/]/i', $xml); @@ -135,11 +136,20 @@ function is_requested_language_available($sxml) { echo "XML_PREVIEW_BEGIN\n".$xmlPreview."\nXML_PREVIEW_END\n"; exit; } + + if ((isset($_REQUEST['diag']) ? $_REQUEST['diag'] : '') == '1') { + scielo_audit_event( + 'access.denied', + 'unauthorized', + 'diagnostics', + null, + array('reason' => 'remote_diag_disabled') + ); + } $pageContent = $scielo->getPage(); - $pageContent .= "\n".''; - $pageContent .= "\n".""; + $pageContent .= "\n".''; if ($GRAVA && $filenamePage){ if (!file_exists($filenamePage)){ diff --git a/htdocs/scieloOrg/php/clinicaltrials.php b/htdocs/scieloOrg/php/clinicaltrials.php index 95a792505..230acd40f 100755 --- a/htdocs/scieloOrg/php/clinicaltrials.php +++ b/htdocs/scieloOrg/php/clinicaltrials.php @@ -5,8 +5,11 @@ $lang = isset($_REQUEST['lang'])?($_REQUEST['lang']):""; $pid = isset($_REQUEST['pid'])?($_REQUEST['pid']):""; - require_once(dirname(__FILE__)."/../../applications/scielo-org/users/langs.php"); - $defFile = parse_ini_file(dirname(__FILE__)."/../../scielo.def.php"); + require_once(dirname(__FILE__)."/../../applications/scielo-org/users/langs.php"); + require_once(dirname(__FILE__)."/../../security.php"); + $defFile = parse_ini_file(dirname(__FILE__)."/../../scielo.def.php"); + $scieloConfig = parse_ini_file(dirname(__FILE__)."/../../scielo.def.php", true); + $internalHost = scielo_internal_host_from_config($scieloConfig); ?>
'ScieloXML/sci_clinicaltrials.xis', + 'pid' => $pid, + )); $xmlFile = file_get_contents($serviceUrl); //die($xmlFile); preg_match_all('//',$xmlFile,$arrResult); diff --git a/htdocs/scieloOrg/php/projfapesp.php b/htdocs/scieloOrg/php/projfapesp.php index d68785586..c4cc79b08 100644 --- a/htdocs/scieloOrg/php/projfapesp.php +++ b/htdocs/scieloOrg/php/projfapesp.php @@ -4,8 +4,11 @@ $lang = isset($_REQUEST['lang'])?($_REQUEST['lang']):""; $pid = isset($_REQUEST['pid'])?($_REQUEST['pid']):""; - require_once(dirname(__FILE__)."/../../applications/scielo-org/users/langs.php"); - $defFile = parse_ini_file(dirname(__FILE__)."/../../scielo.def.php"); + require_once(dirname(__FILE__)."/../../applications/scielo-org/users/langs.php"); + require_once(dirname(__FILE__)."/../../security.php"); + $defFile = parse_ini_file(dirname(__FILE__)."/../../scielo.def.php"); + $scieloConfig = parse_ini_file(dirname(__FILE__)."/../../scielo.def.php", true); + $internalHost = scielo_internal_host_from_config($scieloConfig); ?> @@ -80,7 +83,10 @@
'ScieloXML/sci_projfapesp.xis', + 'pid' => $pid, + )); $xmlFile = file_get_contents($serviceUrl); diff --git a/htdocs/scielolog.php b/htdocs/scielolog.php index b4086b903..0bb87dccd 100644 --- a/htdocs/scielolog.php +++ b/htdocs/scielolog.php @@ -2,7 +2,7 @@ include ("classScieloLogOfi.php"); // Create new Scielo object - $host = $HTTP_HOST; + $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ''; $scielo = new ScieloLog ($host); $script = $_REQUEST["script"]; $dti = $_REQUEST["dti"]; diff --git a/htdocs/scielologofi.php b/htdocs/scielologofi.php index 4cce54825..7033566cf 100644 --- a/htdocs/scielologofi.php +++ b/htdocs/scielologofi.php @@ -2,7 +2,7 @@ include ("classScieloLogOfi.php"); // Create new Scielo object - $host = $HTTP_HOST; + $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ''; $scielo = new ScieloLog ($host); $scielo->SetPreferedMethod ("POST"); diff --git a/htdocs/security.php b/htdocs/security.php new file mode 100644 index 000000000..55947e83b --- /dev/null +++ b/htdocs/security.php @@ -0,0 +1,133 @@ + $itemValue) { + $clean[$itemKey] = scielo_audit_sanitize($itemValue, $itemKey); + } + return $clean; + } + + if (is_string($value)) { + return substr($value, 0, 200); + } + + if (is_scalar($value) || $value === null) { + return $value; + } + + return gettype($value); +} + +function scielo_audit_event($action, $result, $resource, $resourceId = null, $extra = array()) +{ + $ipAddress = isset($_SERVER['REMOTE_ADDR']) + && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP) + ? $_SERVER['REMOTE_ADDR'] + : null; + + $sessionId = null; + if (function_exists('session_status') && session_status() === PHP_SESSION_ACTIVE) { + $rawSessionId = session_id(); + if ($rawSessionId !== '') { + $sessionId = hash('sha256', $rawSessionId); + } + } + + $entry = array( + 'timestamp' => gmdate('c'), + 'user_id' => null, + 'ip_address' => $ipAddress, + 'session_id' => $sessionId, + 'action' => (string) $action, + 'resource' => (string) $resource, + 'resource_id' => $resourceId === null ? null : substr((string) $resourceId, 0, 128), + 'result' => (string) $result, + 'extra' => scielo_audit_sanitize($extra), + ); + + error_log(json_encode($entry, JSON_UNESCAPED_SLASHES)); +} + +function scielo_is_loopback_request() +{ + $remoteAddress = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ''; + return in_array($remoteAddress, array('127.0.0.1', '::1'), true); +} + +function scielo_diagnostics_allowed() +{ + if (getenv('SCIELO_ENABLE_DIAGNOSTICS') !== '1') { + return false; + } + + return scielo_is_loopback_request(); +} + +function scielo_diagnostic_mode_allowed($mode) +{ + if (strtoupper((string) $mode) === 'XML') { + return scielo_is_loopback_request(); + } + + return scielo_diagnostics_allowed(); +} + +function scielo_validate_internal_host($host) +{ + if (!is_string($host) || strlen($host) > 253) { + return false; + } + + if (!preg_match('/^([A-Za-z0-9][A-Za-z0-9.-]*)(?::([0-9]{1,5}))?$/', $host, $matches)) { + return false; + } + + if (strpos($matches[1], '..') !== false) { + return false; + } + + if (isset($matches[2])) { + $port = (int) $matches[2]; + if ($port < 1 || $port > 65535) { + return false; + } + } + + return $host; +} + +function scielo_internal_host_from_config($config) +{ + if (!is_array($config) + || !isset($config['SCIELO']) + || !isset($config['SCIELO']['SERVER_SCIELO'])) { + throw new RuntimeException('SERVER_SCIELO is not configured'); + } + + $host = scielo_validate_internal_host(trim($config['SCIELO']['SERVER_SCIELO'])); + if ($host === false) { + throw new RuntimeException('SERVER_SCIELO has an invalid format'); + } + + return $host; +} diff --git a/tests/security_regression.php b/tests/security_regression.php new file mode 100644 index 000000000..b46d05231 --- /dev/null +++ b/tests/security_regression.php @@ -0,0 +1,65 @@ + '/attacker', 'SCIELO_TEST_GLOBAL' => 'attacker'); +$_POST = array(); +$_COOKIE = array(); +$_SERVER['REQUEST_URI'] = '/trusted'; +require(__DIR__ . '/../htdocs/old2new.inc'); + +assert_true(!isset($GLOBALS['SCIELO_TEST_GLOBAL']), 'request keys must not be copied to GLOBALS'); +assert_true($_SERVER['REQUEST_URI'] === '/trusted', 'request data must not overwrite SERVER values'); +assert_true(isset($GLOBALS['HTTP_GET_VARS']), 'legacy request array alias should remain available'); + +$config = array('SCIELO' => array('SERVER_SCIELO' => '127.0.0.1:8080')); +assert_true(scielo_internal_host_from_config($config) === '127.0.0.1:8080', 'configured internal host should be returned'); +assert_true(scielo_audit_sanitize('secret-value', 'password') === '***', 'sensitive audit fields must be redacted'); + +$old2newSource = file_get_contents(__DIR__ . '/../htdocs/old2new.inc'); +$exportSource = file_get_contents(__DIR__ . '/../htdocs/export.php'); +$articlePageSource = file_get_contents(__DIR__ . '/../htdocs/applications/scielo-org/pages/services/articleRequestGraphicPage.php'); +$sendMailSource = file_get_contents(__DIR__ . '/../htdocs/applications/scielo-org/pages/services/sendMail.php'); +$rssSource = file_get_contents(__DIR__ . '/../htdocs/rss.php'); +$clinicalTrialsSource = file_get_contents(__DIR__ . '/../htdocs/scieloOrg/php/clinicaltrials.php'); +$projectFapespSource = file_get_contents(__DIR__ . '/../htdocs/scieloOrg/php/projfapesp.php'); + +assert_true(strpos($old2newSource, '$GLOBALS[$key]') === false, 'old2new must not export arbitrary request keys'); +assert_true(strpos($exportSource, "\$_SERVER['HTTP_HOST']") === false, 'export must not use the request Host as backend destination'); +assert_true(strpos($articlePageSource, 'new ArticleService($requestedCaller)') === false, 'article statistics must not use the requested caller as a host'); +assert_true(strpos($sendMailSource, 'new ArticleService($requestedCaller)') === false, 'send mail must not use the requested caller as a host'); +assert_true(strpos($rssSource, "\$_SERVER['HTTP_HOST']") === false, 'RSS backends must not use the request Host'); +assert_true(strpos($clinicalTrialsSource, "\$_SERVER['HTTP_HOST']") === false, 'clinical trials backend must not use the request Host'); +assert_true(strpos($projectFapespSource, "\$_SERVER['HTTP_HOST']") === false, 'FAPESP project backend must not use the request Host'); + +fwrite(STDOUT, "Security regression tests passed" . PHP_EOL);